01 | Android 高级进阶(源码剖析篇) 小而美的日志框架 timber(上)

作者简介:ASCE1885, 《Android 高级进阶》作者。
本文由于潜在的商业目的,未经授权不开放全文转载许可,谢谢!
本文分析的源码版本已经 fork 到我的 Github

1519881455332.jpg

无论是前端开发还是后端开发,日志记录都是一个不可或缺的底层基础模块,本文剖析的 timber 是 JakeWharton 开源的一个小而美的日志框架,它是在 Android 系统 Log 类基础上封装的,对外提供可扩展的 API。开发者可以方便快捷的集成不同类型的日志记录方式,例如打印日志到 Logcat,打印日志到文件,打印日志到网络等等,timber 通过一行代码就可以同时调用这多种方式。

timber 源码工程有三个子模块:

  • timber:源码模块,timber 的核心代码都在这里,当然由于功能本身很简单,所以只有一个 .java 文件。
  • timber-lint:timber 提供的自定义 Lint 检查规则,timber 模块依赖于它。
  • timber-sample:timber 的示例模块。

下面我们会分两篇文章分别重点介绍 timber 的核心原理和自定义 Lint Check 的原理和实现。

森林和树

timber 的核心思想很简单,就是维护一个森林对象,它由不同类型的日志树组合而成,例如 Logcat 记录树,文件记录树,网络记录树等等,森林对象提供对外的接口进行日志的打印。每种类型的树都可以通过种植操作来把自己添加到森林对象中,或者通过移除操作从森林对象中删除,从而实现该类型日志记录的开启和关闭。

代码实现中,森林对象是以列表和数组两种形式展现的,代码如下所示。

private static final Tree[] TREE_ARRAY_EMPTY = new Tree[0];
private static final List<Tree> FOREST = new ArrayList<>();
static volatile Tree[] forestAsArray = TREE_ARRAY_EMPTY;

读者可能会有疑问,为什么既要维护一个树的列表,又要维护一个树的数组呢?这样不就存在数据冗余了吗?其实不然。timber 作为一个日志记录框架,开发者可能在主线程中使用它,也可能在子线程中使用它,这时就可能存在多线程同步问题。这个我们后面会进一步分析到。

森林是由一颗一颗的树组成,从上面森林对象的定义也可以看到树对象 Tree,现在我们先来看看树的种植和移除,可以一次种植一棵树,也可以一次种植多棵树,这分别对应到如下两个静态方法:

/** 一次种植一棵树. */
public static void plant(Tree tree) {
    ...
    
    synchronized (FOREST) {
      FOREST.add(tree);
      forestAsArray = FOREST.toArray(new Tree[FOREST.size()]);
    }
}

/** 一次种植多棵树. */
public static void plant(Tree... trees) {
    ...
    
    synchronized (FOREST) {
      Collections.addAll(FOREST, trees);
      forestAsArray = FOREST.toArray(new Tree[FOREST.size()]);
    }
}

可以看到,树的种植是在 synchronized 同步代码块中进行的,一棵树的种植是先将树对象添加到 FOREST 列表中,然后根据 FOREST 列表生成 forestAsArray 数组;多棵树的种植是以集合形式把多个树对象同时添加到 FOREST 列表中,然后根据 FOREST 列表生成 forestAsArray 数组。

同样的,树的移除也是对 FORESTforestAsArray 的操作:

/** 移除森林中一棵树. */
public static void uproot(Tree tree) {
    synchronized (FOREST) {
      if (!FOREST.remove(tree)) {
        throw new IllegalArgumentException("Cannot uproot tree which is not planted: " + tree);
      }
      forestAsArray = FOREST.toArray(new Tree[FOREST.size()]);
    }
}

/** 移除森林中所有的树. */
public static void uprootAll() {
    synchronized (FOREST) {
      FOREST.clear();
      forestAsArray = TREE_ARRAY_EMPTY;
    }
}

跟人类社会一样,森林中的树也存在等级之分,其中有一个高等级的存在,名为灵魂之树 TREE_OF_SOULS,其他的都是普通的树对象,从树种植代码 plant 中也可以看出 TREE_OF_SOULS 的特殊之处,它天然就存在,不需要也不允许开发者手动种植。

/** 一次种植一棵树. */
public static void plant(Tree tree) {
    ...
    
    // 如果开发者手动种植灵魂之树,timber 将会抛出异常
    if (tree == TREE_OF_SOULS) {
      throw new IllegalArgumentException("Cannot plant Timber into itself.");
    }
    
    ...
}

代码实现中,在这里运用的是经典设计模式中的代理模式TREE_OF_SOULS 本质上是一个代理对象,森林中所有其他普通的树对象都是被代理对象,代理对象通过 for 循环来依次调用被代理对象的同名方法,从而实现不同类型的日志记录,如下所示:

private static final Tree TREE_OF_SOULS = new Tree() {
    @Override public void v(String message, Object... args) {
      Tree[] forest = forestAsArray;
      //noinspection ForLoopReplaceableByForEach
      for (int i = 0, count = forest.length; i < count; i++) {
        forest[i].v(message, args);
      }
    }
    
    //...省略 Tree 中定义的其他日志记录方法(v,d,i,w,e,wtf,log)及其重载方法
}

到这里我们就把树的种类,树的种植和移除等讲清楚了,接下来就来解答下前面留下的疑问。我们知道,ArrayList 是非线程安全的,也就是在多线程环境中使用时可能会有问题,典型的是在遍历 ArrayList 的同时进行增删操作将会出现 ConcurrentModificationException 异常。而 timber 的使用场景中,可能存在一个线程在遍历森林中普通树对象进行日志记录的同时,另外一个线程调用 plant 或者 uproot 方法在种植树或者移除树。因此,为了解决这个问题,就出现了前面讲到的森林对象是以列表和数组两种形式展现。通过增加一个数组并在种植树和移除树时重新复制一遍数据来解决 ArrayList 的线程安全问题,具体实现我们可以看看 ArrayList.toArray() 方法,其中的 System.arraycopy 实现数组的复制:

@Override public <T> T[] toArray(T[] contents) {
    int s = size;
    if (contents.length < s) {
        @SuppressWarnings("unchecked") T[] newArray
        = (T[]) Array.newInstance(contents.getClass().getComponentType(), s);
            contents = newArray;
    }
    System.arraycopy(this.array, 0, contents, 0, s);
    if (contents.length > s) {
        contents[s] = null;
    }
    return contents;
}

当然列表 FOREST 和数组 forestAsArray 两者的协作也可以通过使用线程安全的 CopyOnWriteArrayList 来实现,这个数据结构的元素的添加和删除也都是通过复制数组的方法来,我们来看下添加操作的代码:

public synchronized boolean add(E e) {
    Object[] newElements = new Object[elements.length + 1];
    System.arraycopy(elements, 0, newElements, 0, elements.length);
    newElements[elements.length] = e;
    elements = newElements;
    return true;
}

可以看到,为了实现线程安全的操作,除了添加 synchronized 修饰符,本质上都是通过对底层数组进行一次新的复制来实现的,存在一定的性能损耗。

核心算法

timber 日志记录的核心算法在抽象基类 TreeprepareLog 方法中,该方法接收四个参数:

参数 说明
int priority 日志记录优先级,取值同系统 Log,例如 Log.VERBOSE,Log.DEBUG 等
Throwable t 异常信息
String message 正常信息
Object... args message 的可选格式化参数
还有 51% 的精彩内容
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
支付 ¥5.20 继续阅读
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 230,247评论 6 543
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,520评论 3 429
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 178,362评论 0 383
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,805评论 1 317
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,541评论 6 412
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 55,896评论 1 328
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 43,887评论 3 447
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 43,062评论 0 290
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,608评论 1 336
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,356评论 3 358
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,555评论 1 374
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 39,077评论 5 364
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,769评论 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,175评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,489评论 1 295
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,289评论 3 400
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,516评论 2 379

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,734评论 25 708
  • 简介 Timber 是Android大神 Jake Wharton 开发的一套基于Android日志的小型可扩展日...
    Whyn阅读 1,826评论 1 2
  • 当我白发苍苍,你是否还在我的身旁? 当我白发苍苍,我的子女是否是我期望的模样? 当我白发苍苍,我的身体是否还健壮?...
    小草神客阅读 305评论 0 3
  • 2017年8月3日 向战友伊兰、正确脚步致敬: 伊兰确实在很多方面都走到了我们的前面,在很多方面都是我们学习的榜样...
    余良阅读 175评论 1 2
  • 现在几乎每晚都要到十二一点困到不行了才肯放下手机。明明没有什么事情却还是端着手机。或刷剧或刷文字,更觉得没事做的时...
    championone阅读 669评论 0 0