作者簡介:ASCE1885, 《Android 高級(jí)進(jìn)階》作者。
本文由于潛在的商業(yè)目的,未經(jīng)授權(quán)不開放全文轉(zhuǎn)載許可,謝謝!
本文分析的源碼版本已經(jīng) fork 到我的 Github。

無論是前端開發(fā)還是后端開發(fā),日志記錄都是一個(gè)不可或缺的底層基礎(chǔ)模塊,本文剖析的 timber 是 JakeWharton 開源的一個(gè)小而美的日志框架,它是在 Android 系統(tǒng) Log 類基礎(chǔ)上封裝的,對(duì)外提供可擴(kuò)展的 API。開發(fā)者可以方便快捷的集成不同類型的日志記錄方式,例如打印日志到 Logcat,打印日志到文件,打印日志到網(wǎng)絡(luò)等等,timber 通過一行代碼就可以同時(shí)調(diào)用這多種方式。
timber 源碼工程有三個(gè)子模塊:
-
timber:源碼模塊,timber 的核心代碼都在這里,當(dāng)然由于功能本身很簡單,所以只有一個(gè).java文件。 -
timber-lint:timber 提供的自定義 Lint 檢查規(guī)則,timber模塊依賴于它。 -
timber-sample:timber 的示例模塊。
下面我們會(huì)分兩篇文章分別重點(diǎn)介紹 timber 的核心原理和自定義 Lint Check 的原理和實(shí)現(xiàn)。
森林和樹
timber 的核心思想很簡單,就是維護(hù)一個(gè)森林對(duì)象,它由不同類型的日志樹組合而成,例如 Logcat 記錄樹,文件記錄樹,網(wǎng)絡(luò)記錄樹等等,森林對(duì)象提供對(duì)外的接口進(jìn)行日志的打印。每種類型的樹都可以通過種植操作來把自己添加到森林對(duì)象中,或者通過移除操作從森林對(duì)象中刪除,從而實(shí)現(xiàn)該類型日志記錄的開啟和關(guān)閉。
代碼實(shí)現(xiàn)中,森林對(duì)象是以列表和數(shù)組兩種形式展現(xiàn)的,代碼如下所示。
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;
讀者可能會(huì)有疑問,為什么既要維護(hù)一個(gè)樹的列表,又要維護(hù)一個(gè)樹的數(shù)組呢?這樣不就存在數(shù)據(jù)冗余了嗎?其實(shí)不然。timber 作為一個(gè)日志記錄框架,開發(fā)者可能在主線程中使用它,也可能在子線程中使用它,這時(shí)就可能存在多線程同步問題。這個(gè)我們后面會(huì)進(jìn)一步分析到。
森林是由一顆一顆的樹組成,從上面森林對(duì)象的定義也可以看到樹對(duì)象 Tree,現(xiàn)在我們先來看看樹的種植和移除,可以一次種植一棵樹,也可以一次種植多棵樹,這分別對(duì)應(yīng)到如下兩個(gè)靜態(tài)方法:
/** 一次種植一棵樹. */
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 同步代碼塊中進(jìn)行的,一棵樹的種植是先將樹對(duì)象添加到 FOREST 列表中,然后根據(jù) FOREST 列表生成 forestAsArray 數(shù)組;多棵樹的種植是以集合形式把多個(gè)樹對(duì)象同時(shí)添加到 FOREST 列表中,然后根據(jù) FOREST 列表生成 forestAsArray 數(shù)組。
同樣的,樹的移除也是對(duì) 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 (FOREST) {
FOREST.clear();
forestAsArray = TREE_ARRAY_EMPTY;
}
}
跟人類社會(huì)一樣,森林中的樹也存在等級(jí)之分,其中有一個(gè)高等級(jí)的存在,名為靈魂之樹 TREE_OF_SOULS,其他的都是普通的樹對(duì)象,從樹種植代碼 plant 中也可以看出 TREE_OF_SOULS 的特殊之處,它天然就存在,不需要也不允許開發(fā)者手動(dòng)種植。
/** 一次種植一棵樹. */
public static void plant(Tree tree) {
...
// 如果開發(fā)者手動(dòng)種植靈魂之樹,timber 將會(huì)拋出異常
if (tree == TREE_OF_SOULS) {
throw new IllegalArgumentException("Cannot plant Timber into itself.");
}
...
}
代碼實(shí)現(xiàn)中,在這里運(yùn)用的是經(jīng)典設(shè)計(jì)模式中的代理模式,TREE_OF_SOULS 本質(zhì)上是一個(gè)代理對(duì)象,森林中所有其他普通的樹對(duì)象都是被代理對(duì)象,代理對(duì)象通過 for 循環(huán)來依次調(diào)用被代理對(duì)象的同名方法,從而實(shí)現(xiàn)不同類型的日志記錄,如下所示:
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 是非線程安全的,也就是在多線程環(huán)境中使用時(shí)可能會(huì)有問題,典型的是在遍歷 ArrayList 的同時(shí)進(jìn)行增刪操作將會(huì)出現(xiàn) ConcurrentModificationException 異常。而 timber 的使用場景中,可能存在一個(gè)線程在遍歷森林中普通樹對(duì)象進(jìn)行日志記錄的同時(shí),另外一個(gè)線程調(diào)用 plant 或者 uproot 方法在種植樹或者移除樹。因此,為了解決這個(gè)問題,就出現(xiàn)了前面講到的森林對(duì)象是以列表和數(shù)組兩種形式展現(xiàn)。通過增加一個(gè)數(shù)組并在種植樹和移除樹時(shí)重新復(fù)制一遍數(shù)據(jù)來解決 ArrayList 的線程安全問題,具體實(shí)現(xiàn)我們可以看看 ArrayList.toArray() 方法,其中的 System.arraycopy 實(shí)現(xiàn)數(shù)組的復(fù)制:
@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;
}
當(dāng)然列表 FOREST 和數(shù)組 forestAsArray 兩者的協(xié)作也可以通過使用線程安全的 CopyOnWriteArrayList 來實(shí)現(xiàn),這個(gè)數(shù)據(jù)結(jié)構(gòu)的元素的添加和刪除也都是通過復(fù)制數(shù)組的方法來,我們來看下添加操作的代碼:
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;
}
可以看到,為了實(shí)現(xiàn)線程安全的操作,除了添加 synchronized 修飾符,本質(zhì)上都是通過對(duì)底層數(shù)組進(jìn)行一次新的復(fù)制來實(shí)現(xiàn)的,存在一定的性能損耗。
核心算法
timber 日志記錄的核心算法在抽象基類 Tree 的 prepareLog 方法中,該方法接收四個(gè)參數(shù):
| 參數(shù) | 說明 |
|---|---|
| int priority | 日志記錄優(yōu)先級(jí),取值同系統(tǒng) Log,例如 Log.VERBOSE,Log.DEBUG 等 |
| Throwable t | 異常信息 |
| String message | 正常信息 |
| Object... args | message 的可選格式化參數(shù) |