UDN-企业互联网技术人气社区

板块导航

浏览  : 1865
回复  : 0

[其它] 开源日志库Logger的剖析

[复制链接]
呵呵燕的头像 楼主
发表于 2016-9-17 18:59:51 | 显示全部楼层 |阅读模式
  库的整体架构图

4.jpg

  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);

  3.   }
复制代码


  是个接口,那我们就要找到这个接口的实现类,找到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.   /**

  5.   * This method is synchronized in order to avoid messy of logs' order.

  6.   */

  7.   private synchronized void log(int priority, Throwable throwable, String msg, Object... args) {

  8.   // 判断当前设置的日志级别,为NONE则不打印日志

  9.   if (settings.getLogLevel() == LogLevel.NONE) {

  10.   return;

  11.   }

  12.   // 获取tag

  13.   String tag = getTag();

  14.   // 创建打印的消息

  15.   String message = createMessage(msg, args);

  16.   // 打印

  17.   log(priority, tag, message, throwable);

  18.   }

  19.   public enum LogLevel {

  20.   /**

  21.   * Prints all logs

  22.   */

  23.   FULL,

  24.   /**

  25.   * No log will be printed

  26.   */

  27.   NONE

  28.   }
复制代码


  首先,log方法是一个线程安全的同步方法,为了防止日志打印时候顺序的错乱,在多线程环境下,这是非常有必要的。

  其次,判断日志配置的打印级别,FULL打印全部日志,NONE不打印日志。

  再来,getTag():

  1.   private final ThreadLocal localTag = new ThreadLocal<>();

  2.   /**

  3.   * @return the appropriate tag based on local or global */

  4.   private String getTag() {

  5.   // 从ThreadLocal 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/singwhatiwanna/article/details/48350919

  接着,createMessage方法:

  1.   private String createMessage(String message, Object... args) {

  2.   return args == null || args.length == 0 ? message : String.format(message, args);

  3.   }
复制代码


  这里就很清楚了,为什么我们用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);

  50.   }
复制代码


  我们重点来看看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.   }

  42.   }
复制代码


3.png

  方法部分的拼接效果

  接下来看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.   }

  8.   }
复制代码


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

2.png

  漂亮的json显示格式

  上面说了大半天,都还没看到具体的打印是啥,现在来看看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类:

1.png

  类的实现

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

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

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

原文作者:JerryloveEmily   来源:开发者头条
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关于我们
联系我们
  • 电话:010-86393388
  • 邮件:udn@yonyou.com
  • 地址:北京市海淀区北清路68号
移动客户端下载
关注我们
  • 微信公众号:yonyouudn
  • 扫描右侧二维码关注我们
  • 专注企业互联网的技术社区
版权所有:用友网络科技股份有限公司82041 京ICP备05007539号-11 京公网网备安1101080209224 Powered by Discuz!
快速回复 返回列表 返回顶部