0
点赞
收藏
分享

微信扫一扫

01 | 小而美的日志框架 timber(上)核心原理

无论是前端开发还是后端开发,日志记录都是一个不可或缺的底层基础模块,本文剖析的 ​​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

读者可能会有疑问,为什么既要维护一个树的列表,又要维护一个树的数组呢?这样不就存在数据冗余了吗?其实不然。​​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

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

同样的,树的移除也是对 ​​FOREST​​​ 和 ​​forestAsArray​​ 的操作:

/** 移除森林中一棵树. */
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

跟人类社会一样,森林中的树也存在等级之分,其中有一个高等级的存在,名为灵魂之树 ​​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

当然列表 ​​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​​​ 日志记录的核心算法在抽象基类 ​​Tree​​​ 的 ​​prepareLog​​ 方法中,该方法接收四个参数:

参数

说明

int priority

日志记录优先级,取值同系统 Log,例如 Log.VERBOSE,Log.DEBUG 等

Throwable t

异常信息

String message

正常信息

Object… args

message 的可选格式化参数

总结起来 ​​prepareLog​​ 算法流程如下:

  • 获取当前线程的 tag
  • 根据 tag 和日志优先级 priority 判断是否进行日志记录
  • 当正常信息 message 和异常信息 t 都是 null 时,说明没有信息可以记录,方法直接返回
  • 当正常信息 message 和异常信息 t 都不为空时,将两者拼接起来一起输出
  • 异常信息 t 通过​​getStackTraceString​​ 方法转换为字符串
  • 正常信息 message 和可选格式化参数 args 通过​​formatMessage​​ 方法拼装成一个字符串
  • 最后调用抽象方法​​log​​​ 进行日志记录,这个方法由​​Tree​​​ 的子类来实现,后面我们讲​​DebugTree​​ 的时候还会介绍到



举报

相关推荐

0 条评论