01 | Android 高級(jí)進(jìn)階(源碼剖析篇) 小而美的日志框架 timber(上)

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

1519881455332.jpg

無論是前端開發(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ì) 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;
    }
}

跟人類社會(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 日志記錄的核心算法在抽象基類 TreeprepareLog 方法中,該方法接收四個(gè)參數(shù):

參數(shù) 說明
int priority 日志記錄優(yōu)先級(jí),取值同系統(tǒng) Log,例如 Log.VERBOSE,Log.DEBUG 等
Throwable t 異常信息
String message 正常信息
Object... args message 的可選格式化參數(shù)
還有 51% 的精彩內(nèi)容
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
支付 ¥5.20 繼續(xù)閱讀

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,725評(píng)論 25 709
  • 簡介 Timber 是Android大神 Jake Wharton 開發(fā)的一套基于Android日志的小型可擴(kuò)展日...
    Whyn閱讀 1,967評(píng)論 1 2
  • 當(dāng)我白發(fā)蒼蒼,你是否還在我的身旁? 當(dāng)我白發(fā)蒼蒼,我的子女是否是我期望的模樣? 當(dāng)我白發(fā)蒼蒼,我的身體是否還健壯?...
    小草神客閱讀 364評(píng)論 0 3
  • 2017年8月3日 向戰(zhàn)友伊蘭、正確腳步致敬: 伊蘭確實(shí)在很多方面都走到了我們的前面,在很多方面都是我們學(xué)習(xí)的榜樣...
    余良閱讀 189評(píng)論 1 2
  • 現(xiàn)在幾乎每晚都要到十二一點(diǎn)困到不行了才肯放下手機(jī)。明明沒有什么事情卻還是端著手機(jī)。或刷劇或刷文字,更覺得沒事做的時(shí)...
    championone閱讀 736評(píng)論 0 0

友情鏈接更多精彩內(nèi)容