日志框架Timber原理剖析

Timber是JakeWharton大佬开源的日志打印工具类,这个日志打印框架非常简单,但是也非常灵活。

一、为什么选择Timber?

自动生成TAG

不需要手动指定TAG,Timber自动将调用者的类名作为TAG。

可扩展性

通过Timber中plant方法可以定制各类Tree,满足各种使用场景。如release环境下和debug环境下的区分。

支持格式化的字符串

支持使用格式说明符(例如%s)对日志消息进行惰性格式化,这可以通过仅在实际输出日志时对字符串进行格式化来提高性能。

二、如何使用Timber?

1 添加依赖

repositories {
  mavenCentral()
}

dependencies {
  implementation 'com.jakewharton.timber:timber:5.0.1'
}

2 配置

public class ExampleApp extends Application {
  @Override public void onCreate() {
    super.onCreate();

    if (BuildConfig.DEBUG) {
      Timber.plant(new DebugTree());
    } else {
      Timber.plant(new CrashReportingTree());
    }
  }

  /** A tree which logs important information for crash reporting. */
  private static class CrashReportingTree extends Timber.Tree {
    @Override protected void log(int priority, String tag, @NonNull String message, Throwable t) {
      if (priority == Log.VERBOSE || priority == Log.DEBUG) {
        return;
      }

      FakeCrashLibrary.log(priority, tag, message);

      if (t != null) {
        if (priority == Log.ERROR) {
          FakeCrashLibrary.logError(t);
        } else if (priority == Log.WARN) {
          FakeCrashLibrary.logWarning(t);
        }
      }
    }
  }
}

3 使用Timber来记录日志

 //设置临时的tag来输出日志
Timber.tag("LifeCycles");
Timber.d("Activity Created");
//logcat中输出日志
2025-08-22 19:12:42.672  8489-8489  LifeCycles              com.example.timber                   D  Activity Created

//默认以当前调用者类名作为tag来输出日志
Timber.i("A button with ID %s was clicked to say '%s'.", button.getId(), button.getText());
//logcat中输出日志
2025-08-22 19:13:26.755  8489-8489  DemoActivity            com.example.timber                   I  A button with ID 2130771968 was clicked to say 'Hello'.

//如果想每次都指定tag来输出
Timber.tag("tag").d("%s", "arg0", "arg1");

三、Timber源码拆解

1 理解Timber(木材)、Forest(森林)、Tree(树

Timber有3个内部类。

命名非常形象也好理解,Timber(木材)取自Forest(森林),Forest(森林)是由很多的Tree(树)组成。

当我们需要Timber(木材时),会先看下Forest(森林)中有多少棵Tree(树),最终取的是Tree(树)。

当使用Timber.d(xxx)时,实际调用的是Forest类中相应的方法,接着会遍历种植的树,这些树存放在treeArray中,然后会调用Tree类中相应的方法,经过Tree类对需要打印的内容处理后最终通过plant(xxx)种植的树来输出日志。

2 DebugTree

DebugTree是作者默认实现的类,该类继承自Tree,重写了tag属性和log()方法。

DebugTree源码:

open class DebugTree : Tree() {
    private val fqcnIgnore = listOf(
        Timber::class.java.name,
        Forest::class.java.name,
        Tree::class.java.name,
        DebugTree::class.java.name
    )

    override val tag: String?
      get() = super.tag ?: Throwable().stackTrace
          .first { it.className !in fqcnIgnore }
          .let(::createStackElementTag)

    /**
     * Extract the tag which should be used for the message from the `element`. By default
     * this will use the class name without any anonymous class suffixes (e.g., `Foo$1`
     * becomes `Foo`).
     *
     * Note: This will not be called if a [manual tag][.tag] was specified.
    */
    protected open fun createStackElementTag(element: StackTraceElement): String? {
      var tag = element.className.substringAfterLast('.')
      val m = ANONYMOUS_CLASS.matcher(tag)
      if (m.find()) {
        tag = m.replaceAll("")
      }
      // Tag length limit was removed in API 26.
      return if (tag.length <= MAX_TAG_LENGTH || Build.VERSION.SDK_INT >= 26) {
        tag
      } else {
        tag.substring(0, MAX_TAG_LENGTH)
      }
    }

    /**
     * Break up `message` into maximum-length chunks (if needed) and send to either
     * [Log.println()][Log.println] or
     * [Log.wtf()][Log.wtf] for logging.
     *
     * {@inheritDoc}
    */
    override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
      if (message.length < MAX_LOG_LENGTH) {
        if (priority == Log.ASSERT) {
          Log.wtf(tag, message)
        } else {
          Log.println(priority, tag, message)
        }
        return
      }

      // Split by line, then ensure each line can fit into Log's maximum length.
      var i = 0
      val length = message.length
      while (i < length) {
        var newline = message.indexOf('\n', i)
        newline = if (newline != -1) newline else length
        do {
          val end = Math.min(newline, i + MAX_LOG_LENGTH)
          val part = message.substring(i, end)
          if (priority == Log.ASSERT) {
            Log.wtf(tag, part)
          } else {
            Log.println(priority, tag, part)
          }
          i = end
        } while (i < newline)
        i++
      }
    }

    companion object {
      private const val MAX_LOG_LENGTH = 4000
      private const val MAX_TAG_LENGTH = 23
      private val ANONYMOUS_CLASS = Pattern.compile("(\\$\\d+)+$")
    }
  }

tag属性默认会使用通过Timer.tag(xx)指定的tag,如果没有指定的话,会使用调用者的类名。createStackElementTag(xx)就是获取调用者类名的实现过程。

重写的log(xx)方法实现过程也是非常精妙的。因为logcat中有输出4000个字符输出长度的限制,log(xx)方法将超过4000个字符的字符串按4000个字符为一组来分组输出。

抽象类Tree中会对要输出的字符串进行预处理,主要通过如下方法来实现:

private fun prepareLog(priority: Int, t: Throwable?, message: String?, vararg args: Any?) {
  // Consume tag even when message is not loggable so that next message is correctly tagged.
  val tag = tag
  if (!isLoggable(tag, priority)) {
    return
  }

  var message = message
  if (message.isNullOrEmpty()) {
    if (t == null) {
      return  // Swallow message if it's null and there's no throwable.
    }
    message = getStackTraceString(t)
  } else {
    if (args.isNotEmpty()) {
      message = formatMessage(message, args)
    }
    if (t != null) {
      message += "\n" + getStackTraceString(t)
    }
  }

  log(priority, tag, message, t)
}

/** Formats a log message with optional arguments. */
actual protected open fun formatMessage(message: String, args: Array<out Any?>): String = message.format(*args)

3 高级用法

日常使用中,默认种植(plant)DebugTree即可,但比如想要针对release和debug环境特殊处理,则需要plant自己定制化的Tree。如上面ExampleApp中种植的CrashReportingTree那样,特殊级别的日志可以输出到本地文件中,然后在特殊的时机通过App来触发上传本地文件到云端。这在SDK开发的场景中非常常见,比如你是一家提供SDK的供应商,有很多客户在使用,客户在使用的过程中难免会遇到各种问题,问题需要日志来定位,你就可以在关键地方加上日志输出到本地文件中,然后在特殊的地方触发上传即可。

通过上述Timber源码的分析,相信对它的全貌都有了一定的了解。

原创不易,转载请注明出处:https://www.longdw.com