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