安卓to鸿蒙系列:Timber 原创 精华

没用的喵叔
发布于 2021-4-23 19:56
浏览
2收藏

@toc

Guide

本文基于https://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]);
    }
    
  2. synchronized的使用,因为FOREST为单例,所以对其读写要加锁。

  3. static volatile Tree[] forestAsArray ,volatile 保证了可见性

  4. 关于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()]);
        }
      }
    
    • 为什么要把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;
  }
}

2021-04-25更新

上文遗留一个问题:

如果每次都new一个label,太低效,所以这里可以优化。比如如果和上次一样,就使用上次的。或者使用对象池技术。

我在master分支中,默认实现只缓存一个label。后来,在mydev_lru分支中使用LinkedHashMap实例的lru算法,缓存了最近使用的8个label。

不过,最后觉得这样做没有太大的必要。也许引入LinkedHashMap 比构建几个HiLogLabel的代价要更大,还提高了复杂性。作为一个分支提交上去,希望和大家一起思考这个问题。如果大家有更好的想法,可以一起讨论。

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
标签
已于2021-4-25 15:40:43修改
1
收藏 2
回复
举报
2条回复
按时间正序
/
按时间倒序
奶盖
奶盖

感谢分享。

回复
2021-4-25 10:20:27
Whyalone
Whyalone

前排学习,希望能够分享更多安卓移植到鸿蒙相关的内容

回复
2021-4-25 11:11:53
回复
    相关推荐