Timber 源码解析

简介

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<Tree> 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<String> 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}).
     * <p>
     * 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 源码的全部解析过程。
如有差错,烦请指出,感激不尽 ^-^。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容