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