开源日志库Logger的剖析

开源
上一篇介绍了开源日志库Logger的使用,今天主要来分析Logger实现的原理。

上一篇介绍了开源日志库Logger的使用,今天主要来分析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)方法设置进去就可以。

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

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

责任编辑:庞桂玉 来源: segmentfault
相关推荐

2016-09-22 19:31:30

开源日志库Logger

2010-02-05 15:33:29

Android JDK

2012-12-03 10:44:00

开源

2012-07-10 16:22:01

开源架构

2010-02-04 11:05:56

ibmdw虚拟化

2019-11-27 14:41:50

Java技术语言

2009-10-28 13:44:40

linux库文件路径

2010-02-03 11:26:28

2010-04-21 11:43:33

Oracle数据库

2010-01-07 18:03:03

Linux动态库

2019-04-22 15:40:33

2018-09-27 11:25:07

开源日志聚合

2010-05-20 18:05:38

2010-05-31 17:56:27

2021-08-10 08:52:15

微软GCToolkit工具

2010-04-12 15:17:40

dump Oracle

2021-04-27 09:22:28

鸿蒙HarmonyOS应用

2019-01-30 18:00:21

开源Python库

2023-03-08 21:25:58

开源工具库开箱

2022-05-31 08:53:29

Logger定制化Go
点赞
收藏

51CTO技术栈公众号