Androidto鴻蒙系列:Timber

目錄

Guide

本文基於//gitee.com/andych008/timber_ohos 分析Timber的源碼,及移植到鴻蒙需要做的工作。

大神JakeWharton的Timber是我寫日誌的最愛,幾乎在所有的項目中都用。當然一般我會通過Timber使用Logger,原因很簡單,因為Timber介面簡潔,Logger的輸出樣式好看。常規套路:

FormatStrategy formatStrategy = PrettyFormatStrategy.newBuilder()
        .tag("DwGG")   // (Optional) Global tag for every log. Default PRETTY_LOGGER
        .build();

Logger.addLogAdapter(new AndroidLogAdapter(formatStrategy));
Timber.plant(new Timber.DebugTree() {
    @Override
    protected void log(int priority, String tag, String message, Throwable t) {
        Logger.log(priority, tag, message, t);
    }
});

當然它的內部實現也一樣完美。咱們往下看。

原理

Timber英文翻譯為**「木材」**。靜態方法Timber.plant(Tree tree)即種樹。每種一棵樹,就擁有一種日誌能力。

比如樹A表示輸出日誌到控制台,樹B表示輸出日誌到文件,樹C輸出到網路。

程式碼實現上,Timber使用了外觀(facade)模式。

Tree類是外觀類,通過plant方法Timber持有Tree類的實例,Timber中的asTree、tag方法將它暴露出去,而對於調用者來說依賴的是抽象類Tree,而不是具體的Tree的實現,如果要更換或者添加Tree類實例,只需要調用plant等相關方法即可,所有調用者使用Tree對象的地方不需要做任何修改,這是符合面向對象依賴倒置原則的一個很好的體現。

另外也使用了委託(delegate)模式。Tree TREE_OF_SOULS把所有的操作都委託給forestAsArray。

更詳細的分析請移步

  1. Timber 源碼解析
  2. Timber源碼解析及涉及知識點總結

知識點

  1. 臨時tag的實現方法

    很簡單,Timber.tag("臨時tag").d(xxx);設置臨時tag。使用一次就刪除。

    為了性能,使用ThreadLocal 以空間換時間。

    public static abstract class Tree {
    final ThreadLocal<String> explicitTag = new ThreadLocal<>();
    
    String getTag() {
      String tag = explicitTag.get();
      if (tag != null) {
        explicitTag.remove();
      }
      return tag;
    }
    }
    
public static class DebugTree extends Tree {

@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]);
}
    • 為什麼要把List<Tree>轉成Tree[]數組?

      解釋這個問題可以參考 深度解析CopyOnWriteArrayList,執行緒安全的ArrayList!,從使用場景上看,Timber對於List<Tree> FOREST讀多寫少,所以只對寫操作加鎖,讀操作(遍歷時)不需要加鎖。其本質上也是讀寫分離的思想,和CopyOnWriteArrayList類似,也是為了性能。

    • 為什麼要用List.toArray(T[] a),而不是List.toArray()?

      不推薦使用 toArray() 無參方法,此方法返回值只能是Object[]類,若強轉將出現ClassCastException錯誤。

移植到鴻蒙

如果Timber沒有默認提供DebugTree,直接拿來就能在鴻蒙上使用。DebugTree這棵樹的能力是在Logcat中輸出日誌。所以移植要做的就是把android.util.Log換成ohos.hiviewdfx.HiLog。

HiLog在tag的基礎上擴展了HiLogLabel的概念。

label = new HiLogLabel(HiLog.DEBUG,0,tag);

如果每次都new一個label,太低效,所以這裡可以優化。比如如果和上次一樣,就使用上次的。或者使用對象池技術。

關鍵程式碼:

public static class DebugTree extends Tree {
  private final ThreadLocal<HiLogLabel> currentLabel = new ThreadLocal<>();
  private final ThreadLocal<String> currentTag = new ThreadLocal<>();


  @Override protected void log(int priority, String tag, @NotNull String message, Throwable t) {
    HiLogLabel label = getHiLogLabel(tag);

    if (message.length() < MAX_LOG_LENGTH) {
      if (priority == HiLog.FATAL) {
        HiLog.fatal(label,message);
      } else  if (priority == HiLog.INFO){
        HiLog.info(label, message);
      }else if (priority == HiLog.WARN){
        HiLog.warn(label, message);
      }else if (priority == HiLog.ERROR){
        HiLog.error(label, message);
      }else if (priority == HiLog.DEBUG){
        HiLog.debug(label, 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 == HiLog.FATAL) {
          HiLog.fatal(label,part);
        }else  if (priority == HiLog.INFO){
          HiLog.info(label, part);
        }else if (priority == HiLog.WARN){
          HiLog.warn(label, part);
        }else if (priority == HiLog.ERROR){
          HiLog.error(label, part);
        }else if (priority == HiLog.DEBUG){
          HiLog.debug(label, part);
        }
        i = end;
      } while (i < newline);
    }
  }

  private HiLogLabel getHiLogLabel(String tag) {
    HiLogLabel label;
    if (tag.equals(currentTag.get())) {
      label = currentLabel.get();
    } else {
      label = new HiLogLabel(HiLog.DEBUG,0,tag);
      currentLabel.set(label);
      currentTag.set(tag);
    }
    return label;
  }
}
  • synchronized的使用,因為FOREST為單例,所以對其讀寫要加鎖。

  • static volatile Tree[] forestAsArray ,volatile 保證了可見性

  • 關於plant(Tree tree)方法中的forestAsArray = FOREST.toArray(new Tree[FOREST.size()]);

      public static void plant(@NotNull 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()]);
        }
      }
    

    作者:沒用的喵叔

    想了解更多內容,請訪問51CTO和華為合作共建的鴻蒙社區://harmonyos.51cto.com/

Tags: