简介

Timber 是Android大神 Jake Wharton 开发的一套基于Android日志的小型可扩展日志框架,是对于日志功能的封装,有点类似装饰器的思想。
基于这种封装思想,让我们可以在基于 Timber 的基础上,实现统一 api 调用,实现不同日志记录功能。

使用方法

  1. Application.onCreate() 里调用 Timber.plant(tree) 种植一棵树;
  2. 调用 Timber 的静态方法进行日志记录;

更多详情请查看:Timber

原理

简单来说,Timber 将不同的日志操作以树(Tree)的概念进行表示,种植一棵树(plant)就拥有了一种日志记录功能,种植多棵树就可以同时实现多种功能的日志记录。比如,在Debug调试版本时,我们可以种植(plant)一棵调试树(DebugTree),对所有的日志进行记录;在Release版本时,我们可以种植一棵(ReleaseTree),只对 Error 信息进行记录;又比如我们可以种植一棵文件树(FileTree),在运行时将日志记录到文件中······

源码解析: timber:4.5.1

老方法,从调用方法来查看源码。

  1. Timber.plant(tree),先来看下 Timber的静态方法:plant
// Both fields guarded by 'FOREST'.  private static final List FOREST = new ArrayList<>();  static volatile Tree[] forestAsArray = TREE_ARRAY_EMPTY;  /** Add a new logging tree. */  public static void plant(Tree tree) {    if (tree == null) {      throw new NullPointerException("tree == null");    }    if (tree == TREE_OF_SOULS) {      throw new IllegalArgumentException("Cannot plant Timber into itself.");    }    synchronized (FOREST) {      FOREST.add(tree);      forestAsArray = FOREST.toArray(new Tree[FOREST.size()]);    }  }

主要就是最后一句代码: forestAsArray = FOREST.toArray(new Tree[FOREST.size()]);,就是将日志树保存到 forestAsArray 数组中(将树种植到森林中)。 同时,采用同步代码块(synchronized)进行日志树存储,所以 Timber.plant 支持多线程环境(支持多线程种植树)。

  1. Timber.d("write your log message"),那么我们就来看下 Timber.d() 源码:
 /** Log a debug message with optional format args. */  public static void d(@NonNls String message, Object... args) {    TREE_OF_SOULS.d(message, args);  }

所以,其实就是调用了 TREE_OF_SOULS 相应的方法,那么我们来看下 TREE_OF_SOULS 是什么呢:

 /** A {@link Tree} that delegates to all planted trees in the {@linkplain #FOREST forest}. */  private static final Tree TREE_OF_SOULS = new Tree() {    ···    ···    @Override public void d(Throwable t, String message, Object... args) {      Tree[] forest = forestAsArray;      //noinspection ForLoopReplaceableByForEach      for (int i = 0, count = forest.length; i < count; i++) {        forest[i].d(t, message, args);      }    }    ···    ···    @Override protected void log(int priority, String tag, String message, Throwable t) {      throw new AssertionError("Missing override for log method.");    }  };

从源码中我们可以看出,TREE_OF_SOULS 就是一棵树(Tree 的实例),这棵树是森林的委托类,全权负责将调用转发到森林中的每棵树中,让所有已种植的树都去实现各自的日记记录功能。

Timber 内部提供了一个调试树(DebugTree),假设我们在Applicaion.onCreate() 里面种植了这棵树(Timber.plant(new DebugTree());),那么,当我们调用 Timber.d("Activity Created") 的时候,就会经由 TREE_OF_SOULS 转发给到森林中(此时森林只有一棵树:DebugTree),那么我们的 DebugTree.d() 就会被调用,在看 DebugTree.d() 源码前,我们先来看下它的父类,也就是实现自定义日志操作功能的很重要的一个类:Tree
首先,看下 Tree 的结构组成:

Tree
从上图可以看到, Tree 封装了基本的日志功能函数( d,i,v,w,e,wtf),接下来我们抽取一些 Tree 源码进行查看:

 /** A facade for handling logging calls. Install instances via {@link #plant Timber.plant()}. */  public static abstract class Tree {    final ThreadLocal explicitTag = new ThreadLocal<>();    String getTag() {      String tag = explicitTag.get();      if (tag != null) {        explicitTag.remove();      }      return tag;    }        /** Log a debug exception and a message with optional format args. */    public void d(Throwable t, String message, Object... args) {      prepareLog(Log.DEBUG, t, message, args);    }    /** Return whether a message at {@code priority} or {@code tag} should be logged. */    protected boolean isLoggable(String tag, int priority) {      return isLoggable(priority);    }    private void prepareLog(int priority, Throwable t, String message, Object... args) {      // Consume tag even when message is not loggable so that next message is correctly tagged.      String tag = getTag();      if (!isLoggable(tag, priority)) {        return;      }      if (message != null && message.length() == 0) {        message = null;      }      if (message == null) {        if (t == null) {          return; // Swallow message if it's null and there's no throwable.        }        message = getStackTraceString(t);      } else {        if (args.length > 0) {          message = formatMessage(message, args);        }        if (t != null) {          message += "\n" + getStackTraceString(t);        }      }      log(priority, tag, message, t);    }    /**     * Write a log message to its destination. Called for all level-specific methods by default.     *     * @param priority Log level. See {@link Log} for constants.     * @param tag Explicit or inferred tag. May be {@code null}.     * @param message Formatted log message. May be {@code null}, but then {@code t} will not be.     * @param t Accompanying exceptions. May be {@code null}, but then {@code message} will not be.     */    protected abstract void log(int priority, String tag, String message, Throwable t);  }

Tree 源码中我们可以看出以下几点:

  • Tree.d(),Tree.i(),Tree.v(),Tree.w(),Tree.e(),Tree.wtf()都调用的是 prepareLog(),而prepareLog()主要做了以下几件事:
     1. 获取线程 tag:getTag()
     2. 判断一下是否允许进行日志记录:isLoggable(tag, priority)
     3. 对消息和异常消息进行处理组合;
     4. 调用 log() 函数进行真正的日志记录操作(log() 是纯虚函数,所以真正的日志记录操作由子类进行确定,所以,Timber 的高扩展性其实就是通过纯虚函数来实现的);

然后,我们终于可以回归到前面的内容,接下来就让我们看下 DebugTree.d() 的源码:

DebugTree
从上图可以看到, DebugTree 没有复写父类( Tree)的 d() 函数,所以它调用的就是 Tree.d(),从上文的分析中,我们可以知道, Tree.d()最终调用的就是 prepareLog,而 prepareLog 主要做的就是4件事,其中, DebugTree 复写了 getTag()log()函数,那么我们就主要来看下 DebugTree 复写的这两个函数,然后结合我们上面对 Tree 源码的分析,就可以得出 DebugTree 的功能实现具体思路了:

  1. 首先看下 getTag()
 private static final int CALL_STACK_INDEX = 5; private static final Pattern ANONYMOUS_CLASS = Pattern.compile("(\\$\\d+)+$");   @Override final String getTag() {      String tag = super.getTag();      if (tag != null) {        return tag;      }      // DO NOT switch this to Thread.getCurrentThread().getStackTrace(). The test will pass      // because Robolectric runs them on the JVM but on Android the elements are different.      StackTraceElement[] stackTrace = new Throwable().getStackTrace();      if (stackTrace.length <= CALL_STACK_INDEX) {        throw new IllegalStateException(            "Synthetic stacktrace didn't have enough elements: are you using proguard?");      }      return createStackElementTag(stackTrace[CALL_STACK_INDEX]);    }/**     * Extract the tag which should be used for the message from the {@code element}. By default     * this will use the class name without any anonymous class suffixes (e.g., {@code Foo$1}     * becomes {@code Foo}).     * 

* Note: This will not be called if a {@linkplain #tag(String) manual tag} was specified. */ protected String createStackElementTag(StackTraceElement element) { String tag = element.getClassName(); Matcher m = ANONYMOUS_CLASS.matcher(tag); if (m.find()) { tag = m.replaceAll(""); } tag = tag.substring(tag.lastIndexOf('.') + 1); return tag.length() > MAX_TAG_LENGTH ? tag.substring(0, MAX_TAG_LENGTH) : tag; }

从源码可以看出,DebugTree.getTag() 主要做了以下几件事:

  • 首先从父类(Tree.getTag())获取当前线程设置的tag,如果找到,就直接返回;如果找不到,那么,就获取当前线程的函数调用栈(new Throwable().getStackTrace()),获取函数调用栈索引为5的调用栈信息,然后解析该函数调用栈类名信息,得出类名作为tag。
    注:这里的调用栈为5的是因为我们开始调用 Timber.d()DebugTree.getTag() 函数时,总共经历了6个函数,举例说明如下:
    假设我们是在 DemoActivity.onCreate() 中输出日志:Timber.d("Activity Created");,那么,它的调用栈如下图所示:

stack
从上面的函数调用栈中可以看出,我们在一个类中,经由 Timber.d() 调用到 DebugTree.getTag() 时,总共经历了6个调用栈/函数,所以调用栈索引为5(也就是第6个)就是我们调用 Timber.d() 的类。
如果还有不清楚的地方,可以参考文章: JakeWharton的timber自动生成Tag原理

  • 获取到调用栈类信息后,就调用函数 createStackElementTag() 进行类名获取,对于匿名内部类(形如 Foo$1),只会取其外部类类名作为tag:Foo
    注: createStackElementTag() 函数主要做如下几件事:
     1. 使用正则表达式对匿名内部类进行获取,获取得到就将后缀($1)删除,只保留前缀内容;
     2. 获取类名;
     3. 类名长度最大只取23个字符;

DebugTree.getTag()到这里就分析完了。

  1. 接下来看下 DebugTree.log() 函数源码:
  private static final int MAX_LOG_LENGTH = 4000; /**     * Break up {@code message} into maximum-length chunks (if needed) and send to either     * {@link Log#println(int, String, String) Log.println()} or     * {@link Log#wtf(String, String) Log.wtf()} for logging.     *     * {@inheritDoc}     */    @Override protected void log(int priority, String tag, String message, Throwable t) {      if (message.length() < MAX_LOG_LENGTH) {        if (priority == Log.ASSERT) {          Log.wtf(tag, message);        } else {          Log.println(priority, tag, message);        }        return;      }      // Split by line, then ensure each line can fit into Log's maximum length.      for (int i = 0, length = message.length(); i < length; i++) {        int newline = message.indexOf('\n', i);        newline = newline != -1 ? newline : length;        do {          int end = Math.min(newline, i + MAX_LOG_LENGTH);          String part = message.substring(i, end);          if (priority == Log.ASSERT) {            Log.wtf(tag, part);          } else {            Log.println(priority, tag, part);          }          i = end;        } while (i < newline);      }    }

从源码可以看出,log() 函数主要做了以下几件事:

  • 如果 message 的长度小于4000个字符,那么就根据 priority 标识,输出相应日志到 Log(所以 DebugTree 底层就是调用Android原生系统的 Log 进行日志输出);
  • 如果 message 的长度大于或等于4000个字符,那么就切断输出,具体切断方法如下:
     1. 取出一行长度
     2. 找不到换行符,证明是最后剩余的数据,则获取其长度;
     3. 获取前面获取到的一行数据的长度与4000进行比较,如果小于4000,则根据 priority 标识,输出相应日志到 Log,然后进行下一行的数据长度获取,重复步骤1,2,3; 如果获取的长度大于4000,那么就对这行数据进行分次获取,每次从 message 相应位置取4000个字符的子串,进行日志输出,一直循环发送直到该行遍历完成,然后才进行下一行的数据长度获取,重复步骤1,2,3;

以上,就是 Timber 源码的全部解析过程。
如有差错,烦请指出,感激不尽 ^-^。

更多相关文章

  1. android viewflipper和tabhost效率对比
  2. Android(安卓)SharedPreferences的使用.
  3. changeConfig配置 以及对activity生命周期的影响
  4. Android(安卓)在低版本sdk中没有getSupportedPreviewSizes和getS
  5. Android(安卓)Studio上用真机调试时,无法查看Logcat日志信息解决
  6. Android下可用的wide char 函数库
  7. android: Can't create handler inside thread that has not cal
  8. 关于Android(安卓)Settings中的八个问题
  9. android内存泄露优化总结

随机推荐

  1. MIPS android 编译总结
  2. Android数据与服务器交互的GET,POST,HTTP
  3. APIDEMO 字体默认样式
  4. Android(安卓)SMS(二)—— 读取短信保存到
  5. Android屏幕分辨率
  6. Android按键消息传播流程(WindowManagerSe
  7. android 笔记 --- Android中Menu应用
  8. OrmLite - Lightweight Java ORM Support
  9. TextView的跑马灯效果
  10. Android 4.0 用户输入子系统