开源日志库Logger的剖析

库的整体架构图

详细剖析

我们从使用的角度来对Logger库抽茧剥丝:


  1. String userName = "Jerry"; 
  2. Logger.i(userName);  

看看Logger.i()这个方法:


  1. public static void i(String message, Object... args) {       
  2.     printer.i(message, args); 
  3. }  

还有个可变参数,来看看printer.i(message, args)是啥:


  1. public Interface Printer{ 
  2.     void i(String message, Object... args); 

是个接口,那我们就要找到这个接口的实现类,找到printer对象在Logger类中声明的地方:


  1. private static Printer printer = new LoggerPrinter(); 

实现类是LoggerPrinter,而且这还是个静态的成员变量,这个静态是有用处的,后面会讲到,那就继续跟踪LoggerPrinter类的i(String message, Object... args)方法的实现:


  1. @Override public void i(String message, Object... args) {   
  2.     log(INFO, null, message, args); 
  3. /**  
  4. * This method is synchronized in order to avoid messy of logs' order.  
  5. */ 
  6. private synchronized void log(int priority, Throwable throwable, String msg, Object... args) { 
  7.     // 判断当前设置的日志级别,为NONE则不打印日志   
  8.     if (settings.getLogLevel() == LogLevel.NONE) {     
  9.         return;   
  10.     } 
  11.     // 获取tag 
  12.     String tag = getTag();  
  13.     // 创建打印的消息 
  14.     String message = createMessage(msg, args);       
  15.     // 打印 
  16.     log(priority, tag, message, throwable); 
  17.  
  18. public enum LogLevel {   
  19.     /**    
  20.     * Prints all logs    
  21.     */   
  22.     FULL,   
  23.     /**    
  24.     * No log will be printed    
  25.     */   
  26.     NONE 
  27. }  
  • 首先,log方法是一个线程安全的同步方法,为了防止日志打印时候顺序的错乱,在多线程环境下,这是非常有必要的。
  • 其次,判断日志配置的打印级别,FULL打印全部日志,NONE不打印日志。
  • 再来,getTag(): 
    
    
    1. private final ThreadLocal<String> localTag = new ThreadLocal<>(); 
    2. /**  
    3. * @return the appropriate tag based on local or global */ 
    4. private String getTag() {   
    5.     // 从ThreadLocal<String> localTag里获取本地一个缓存的tag 
    6.     String tag = localTag.get();   
    7.     if (tag != null) {     
    8.         localTag.remove();     
    9.         return tag;   
    10.     }   
    11.     return this.tag; 
    12. }  

这个方法是获取本地或者全局的tag值,当localTag中有tag的时候就返回出去,并且清空localTag的值,关于ThreadLocal还不是很清楚的可以参考主席的文章:http://blog.csdn.net/singwhat...

接着,createMessage方法:


  1. private String createMessage(String message, Object... args) {  
  2.     return args == null || args.length == 0 ? message : String.format(message, args); 

这里就很清楚了,为什么我们用Logger.i(message, args)的时候没有写args,也就是null,也可以打印,而且是直接打印的message消息的原因。同样博主上一篇文章也提到了:


  1. Logger.i("博主今年才%d,英文名是%s", 16, "Jerry"); 

像这样的可以拼接不同格式的数据的打印日志,原来实现的方式是用String.format方法,这个想必小伙伴们在开发Android应用的时候String.xml里的动态字符占位符用的也不少,应该很容易理解这个format方法的用法。

重头戏,我们把tag,打印级别,打印的消息处理好了,接下来该打印出来了:


  1. @Override public synchronized void log(int priority, String tag, String message, Throwable throwable) { 
  2.     // 同样判断一次库配置的打印开关,为NONE则不打印日志 
  3.     if (settings.getLogLevel() == LogLevel.NONE) {     
  4.         return;   
  5.     } 
  6.     // 异常和消息不为空的时候,获取异常的原因转换成字符串后拼接到打印的消息中   
  7.     if (throwable != null && message != null) {     
  8.         message += " : " + Helper.getStackTraceString(throwable);   
  9.     }   
  10.     if (throwable != null && message == null) {     
  11.         message = Helper.getStackTraceString(throwable);   
  12.     }   
  13.     if (message == null) {     
  14.         message = "No message/exception is set";   
  15.     }   
  16.     // 获取方法数 
  17.     int methodCount = getMethodCount();  
  18.     // 判断消息是否为空  
  19.     if (Helper.isEmpty(message)) {     
  20.         message = "Empty/NULL log message";   
  21.     }   
  22.     // 打印日志体的上边界 
  23.     logTopBorder(priority, tag); 
  24.     // 打印日志体的头部内容   
  25.     logHeaderContent(priority, tag, methodCount);   
  26.     //get bytes of message with system's default charset (which is UTF-8 for Android)   
  27.     byte[] bytes = message.getBytes();   
  28.     int length = bytes.length;   
  29.     // 消息字节长度小于等于4000 
  30.     if (length <= CHUNK_SIZE) {     
  31.         if (methodCount > 0) {   
  32.             // 方法数大于0,打印出分割线     
  33.             logDivider(priority, tag);     
  34.         }     
  35.         // 打印消息内容 
  36.         logContent(priority, tag, message); 
  37.         // 打印日志体底部边界 
  38.         logBottomBorder(priority, tag);     
  39.         return;   
  40.     }   
  41.     if (methodCount > 0) {     
  42.         logDivider(priority, tag);   
  43.     }   
  44.     for (int i = 0; i < length; i += CHUNK_SIZE) {     
  45.         int count = Math.min(length - i, CHUNK_SIZE); 
  46.         //create a new String with system's default charset (which is UTF-8 for Android)     
  47.         logContent(priority, tag, new String(bytes, i, count));   
  48.     }   
  49.     logBottomBorder(priority, tag); 

我们重点来看看logHeaderContent方法和logContent方法:


  1. @SuppressWarnings("StringBufferReplaceableByString") 
  2. private void logHeaderContent(int logType, String tag, int methodCount) {   
  3.   // 获取当前线程堆栈跟踪元素数组 
  4.   //(里面存储了虚拟机调用的方法的一些信息:方法名、类名、调用此方法在文件中的行数) 
  5.   // 这也是这个库的 “核心” 
  6.   StackTraceElement[] trace = Thread.currentThread().getStackTrace(); 
  7.   // 判断库的配置是否显示线程信息   
  8.   if (settings.isShowThreadInfo()) { 
  9.       // 获取当前线程的名称,并且打印出来,然后打印分割线     
  10.       logChunk(logType, tag, HORIZONTAL_DOUBLE_LINE + "Thread: " + Thread.currentThread().getName());    logDivider(logType, tag);   
  11.   }   
  12.   String level = "";   
  13.   // 获取追踪栈的方法起始位置 
  14.   int stackOffset = getStackOffset(trace) + settings.getMethodOffset();   
  15.   //corresponding method count with the current stack may exceeds the stack trace. Trims the count   
  16.   // 打印追踪的方法数超过了当前线程能够追踪的方法数,总的追踪方法数扣除偏移量(从调用日志的起算扣除的方法数),就是需要打印的方法数量 
  17.   if (methodCount + stackOffset > trace.length) {     
  18.       methodCount = trace.length - stackOffset - 1;   
  19.   }   
  20.   for (int i = methodCount; i > 0; i--) {    
  21.       int stackIndex = i + stackOffset;     
  22.       if (stackIndex >= trace.length) {       
  23.           continue;     
  24.       }     
  25.       // 拼接方法堆栈调用路径追踪字符串 
  26.       StringBuilder builder = new StringBuilder();  
  27.       builder.append("║ ")         
  28.       .append(level)      
  29.       .append(getSimpleClassName(trace[stackIndex].getClassName()))  // 追踪到的类名 
  30.       .append(".")  
  31.       .append(trace[stackIndex].getMethodName())  // 追踪到的方法名       
  32.       .append(" ")         
  33.       .append(" (")        
  34.       .append(trace[stackIndex].getFileName()) // 方法所在的文件名 
  35.       .append(":")         
  36.       .append(trace[stackIndex].getLineNumber())  // 在文件中的行号       
  37.       .append(")");     
  38.       level += "   ";     
  39.       // 打印出头部信息 
  40.       logChunk(logType, tag, builder.toString());  
  41.   } 

接下来看logContent方法:


  1. private void logContent(int logType, String tag, String chunk) {   
  2.     // 这个作用就是获取换行符数组,getProperty方法获取的就是"\\n"的意思 
  3.     String[] lines = chunk.split(System.getProperty("line.separator"));   
  4.     for (String line : lines) {     
  5.         // 打印出包含换行符的内容 
  6.         logChunk(logType, tag, HORIZONTAL_DOUBLE_LINE + " " + line);   
  7.     } 

如上图来说内容是字符串数组,本身里面是没用换行符的,所以不需要换行,打印出来的效果就是一行,但是json、xml这样的格式是有换行符的,所以打印呈现出来的效果就是:

上面说了大半天,都还没看到具体的打印是啥,现在来看看logChunk方法:


  1. private void logChunk(int logType, String tag, String chunk) { 
  2.     // 最后格式化下tag   
  3.     String finalTag = formatTag(tag);   
  4.     // 根据不同的日志打印类型,然后交给LogAdapter这个接口来打印 
  5.     switch (logType) {     
  6.         case ERROR:       
  7.             settings.getLogAdapter().e(finalTag, chunk);       
  8.         break;     
  9.         case INFO:       
  10.             settings.getLogAdapter().i(finalTag, chunk);       
  11.         break;     
  12.         case VERBOSE:       
  13.             settings.getLogAdapter().v(finalTag, chunk);       
  14.         break;     
  15.         case WARN:       
  16.             settings.getLogAdapter().w(finalTag, chunk);       
  17.         break;    
  18.         case ASSERT:       
  19.             settings.getLogAdapter().wtf(finalTag, chunk);       
  20.         break;     
  21.         case DEBUG:       
  22.             // Fall through, log debug by default     
  23.         default:             
  24.             settings.getLogAdapter().d(finalTag, chunk);       
  25.         break;   
  26.     } 
  27. }  

这个方法很简单,就是最后格式化tag,然后根据不同的日志类型把打印的工作交给LogAdapter接口来处理,我们来看看settings.getLogAdapter()这个方法(Settings.java文件):


  1. public LogAdapter getLogAdapter() {   
  2.     if (logAdapter == null) { 
  3.         // 最终的实现类是AndroidLogAdapter 
  4.         logAdapter = new AndroidLogAdapter();   
  5.     }   
  6.     return logAdapter; 
  7. }  

找到AndroidLogAdapter类:

原来绕了一大圈,最终打印还是使用了:系统的Log。

好了Logger日志框架的源码解析完了,有没有更清晰呢,也许小伙伴会说这个最终的日志打印,我不想用系统的Log,是不是可以换呢。这是自然的,看开篇的那种整体架构图,这个LogAdapter是个接口,只要实现这个接口,里面做你自己想要打印的方式,然后通过Settings 的logAdapter(LogAdapter logAdapter)方法设置进去就可以。

以上就是博主分析一个开源库的思路,从使用的角度出发抽茧剥丝,基本上一个库的核心部分都能搞懂。画画整个框架的大概类图,对分析库非常有帮助,每一个轮子都有值得学习的地方,吸收了就是进步的开始,耐心的分析完一个库,还是非常有成就感的。

感谢你耐心看完,以后博主还会继续努力分析其它轮子的。

作者:jerryloveemily

来源:51CTO

时间: 2024-09-08 22:13:17

开源日志库Logger的剖析的相关文章

开源日志库Logger的使用

日志对于开发来说是非常重要的,不管是调试数据查看.bug问题追踪定位.数据信息收集统计,日常工作运行维护等等,都大量的使用到. 在Android Studio中的gradle中加入,就可以引用依赖logger库: dependencies {    compile 'com.orhanobut:logger:1.15'  }   Logger库能提供的功能 线程的信息 类的信息 方法的信息 格式打印json.xml等 点击链接跳转到源码打印处 Logger的使用 使用非常简单: String u

c++ 开源日志库选择

liblogger 待选为 glog.log4cplus.log4cpp.log4cxx 目前准备使用glog,使用方便,性能也不错,待进一步试验,如果有不能满足的功能就转用 log4cplus,功能很全面,不过稍复杂些. 其它两个都是三年前就没更新,没好感,暂不准备使用. 1.log4cplus 最新版本:1.1.0   2012-03-11 下载地址:http://sourceforge.net/projects/log4cplus/files/log4cplus-stable/1.1.0

C++ 日志库 boost::log 以及 glog 的对比

该文章转自阿里ata精选文章,作者为苏樽 日志能方便地诊断程序原因.统计程序运行数据,是大型软件系统必不可少的组件之一.本文将从设计上和功能上对比 C++ 语言常见的两款日志库: boost::log 和 google-glog .   设计 boost::log 的设计主要有日志器( Logger ).日志核心( Logging core ). Sink 前后端( frontend, backend )组成.日志文本以及日志环境由日志器( Logger )负责搜集,日志核心负责处理日志数据(例

机器视觉开源处理库汇总

机器视觉开源处理库汇总 从cvchina搞到的机器视觉开源处理库汇总,转来了,很给力,还在不断更新... 通用库/General Library OpenCV 无需多言. RAVL Recognition And Vision Library. 线程安全.强大的IO机制.包含AAM. CImg 很酷的一个图像处理包.整个库只有一个头文件.包含一个基于PDE的光流算法. 图像,视频IO/Image, Video IO FreeImage DevIL ImageMagick FFMPEG Video

从Google开源RE2库学习到的C++测试方案

最近因为科研需求,一直在研究Google的开源RE2库(正则表达式识别库),库源码体积庞大,用C++写的,对于我这个以前专供Java的人来说真的是一件很痛苦的事,每天只能啃一点点.今天研究了下里面用到的测试方法,感觉挺好的,拿来跟大家分享下!(哈~C++大牛勿喷) 对于我这个C++菜鸟中的菜鸟而言,平时写几个函数想要测试一般都是在main中一个一个的测试,因为没用C++写过项目,没有N多方法所以在main中一个个测试也不费劲.但是对于一个项目而言,或多或少都有N多方法,如果在main中一个个测试

Logback 1.0.1发布 开源日志系统

Logback 是一个作为log4j项目的继任者,是由Log4j的创始人Ceki Gulcu设计成一款开源日志系统. Logback 配备基本架构以通用于不同情况下的需求,它分为三个模块:Core module.Classic module 和 Access module.Core module核心模块是为其他两个模块设计的一个基础模块.Classic module类似于log4j的改进版本,实现了SLF4J API使你可以很容易地在logback和其它日志系统(log4j 或 JDK14 )之

我的Android进阶之旅------&amp;gt;【强力推荐】Android开源图表库XCL-Charts版本发布及展示页

         因为要做图表相关的应用,后来百度发现了一个很好的Android开源图表库(XCL-Charts is a free charting library for Android platform.)                      下面内容转载于:http://blog.csdn.net/xcl168/article/details/29675613,详细介绍了XCL-Charts的展示内容.   =====================================

C++ 100款开源界面库 (10)

(声明:Alberl以后说到开源库,一般都是指著名的.或者不著名但维护至少3年以上的.那些把代码一扔就没下文的,Alberl不称之为开源库,只称为开源代码.这里并不是贬低,像Alberl前面那个系列的教程<2013 duilib入门简明教程>,还有本系列教程,还有前面介绍的CodeProject,基本上都是代码往上面一扔,就不用再怎么维护的.这些都称之为开源代码,其实开源代码对新手的帮助更大,因为很简明的说明了代码用法~O(∩_∩)O~)       前面两个教程已经对制作界面的几种方式进行了

Disable Google Fonts Plus插件禁用谷歌开源字体库

由于Google Fonts字体库等服务在国内被禁用,许多使用Google Fonts的WordPress网站打开异常困难.今天,我们给大家带来了一款插件,Disable Google Fonts Plus,可以方便地解决这个问题. 为什么要关闭Google Fonts字体库 Google Fonts 是谷歌公司推出的一项字体库服务,主要是为了解决跨平台的字体一致性问题.WordPress 3.8版本在重新设计WordPress后台界面的时候,开始使用Google Fonts字体库. 使用Goo