WhyUsePLog
在编程世界里存在很多这样的例子:标准库往往是万能的,可以解决几乎所有问题~~(如果不能,那就升级标准库)~~, 但它往往也使用起来不很方便,需要二次封装来使用。
就Android开发的日志打印需求来说,标准库提供的android.util.Log
类,使用起来就不够方便。
即使在Logcat中选择了只看当前进程,也会有各种奇怪的日志分散注意力,比如下面这种:
注意,这里的截图仅仅是为了举例说明问题,并不是针对某个ROM。实际上大部分ROM都有各种各样的日志没在release的时候去掉。
标准日志类Log
提供了v/d/i/w/e(String, String, [Throwable])
系列方法,这些方法有一个共同点:必须提供一个tag。这个tag在做筛选的时候很有用,但是每次打日志都需要多传一个参数又很麻烦。
早期Android开发中,我习惯在BaseActivity
里定义一个LOG_TAG
字段, 就像这样:
public class BaseActivity extends Activity {
protected final String LOG_TAG = getClass().getSimpleName();
}
后来,在Fragment里也常常需要打日志,于是又在BaseFragment
里再加一个一模一样的代码。
问题来了,我还可能在工具类、自定义View类和网络接口回调里打日志呢?总不能再搞个基类吧?
如果我们希望打印网络返回日志,而又没有做特殊处理的话,那么我们打印出来的日志很有可能被截断,因为Logcat一行最长只有4000个字符,超出的字符会被截断。当你看着成百上千字符的JSON串挤在一行的时候,你的眼睛还好吗?
上述问题自然有各种各样的解决方案。其中有最简单的只加了一个isDebug
开关的类,也有复杂的日志库。包装类的方式最普遍,但是往往只能解决输出的控制问题,不能解决便利性问题。这里主要讲讲我见过的第三方日志库。
先定义我对日志库比较的几个维度:
- 容易打印:这是最基本、最重要的要求,希望一行代码就能打印任何类型的数据,必要的时候可以指定一些额外参数,但不能写额外的一堆代码。
- 筛选容易:这也是一个非常重要的特性,当日志量大的时候非常有用。一般要求能够自动打印当前类名,另外筛选的维度尽可能要多样化,以应付日志过多的场景。
- 支持定位:Android Studio的控制台会对包含特定格式字符串的文本追加定位功能,在看日志的时候小手一点就可以看到打日志的地方,省时省力。
- 日志美观:可以支持换行和美化效果,但要有一定的度。个人不喜欢过度复杂的花哨日志,相比看一堆线框,我宁愿用标准库。
- 输出可控:不仅要求能多向输出(比如SD卡和第三方Crash监测平台),还要能在需要的时候做拦截和关闭输出的操作。
- 设置灵活:每个APP对日志的需求都不相同,所有非核心信息都应该可以由调用者控制是否显示,比如线程信息、堆栈信息和格式化信息。
- 体积轻巧:日志只是一个辅助开发工具,如果为了使用这个功能要引入额外的大体积依赖包,有点得不偿失。
- 上手简单:学习成本不能过高。
OK,标准定义好了,来看看比较结果(部分选项可能存在主观影响,仅代表个人看法)。
Library Name | Logger | Timber | KLog |
---|---|---|---|
Star/Fork | 5.7K+/1.0K+ | 3.5K+/366 | 1.1K+/251 |
容易打印 | √ | √ | √ |
支持定位 | √ | × | √ |
筛选容易* | ☆ | × | ☆☆ |
日志美观* | ☆ | × | ☆ |
输出可控* | × | ☆☆ | ☆ |
设置灵活 | ☆ | ☆ | ☆ |
体积轻巧 | ☆ | ☆☆ | ☆ |
上手简单 | √ | √ | √ |
所有数据截止日期为 2017-02-16, 17:00.
解释一下:
- 筛选容易:KLog可以支持用类名做Tag,Logger将类名信息放到了消息体中,而Timber并没有这一功能。
- 日志美观:Logger和KLog绘制了行分割线等装饰,Logger更是可以将一行消息打印为8行,过于花哨,反而影响查看日志本身;而Timber则并没有做这方面的处理,也就没有解决Logcat 4K截断问题。
- 输出可控:Logger不支持重定向输出;Timber可以支持同时向多个通道输出日志,但没有统一的拦截方式;**KLog **虽然可以输出到文件,但无法同时多通道打印,也没有统一的拦截方式。
按惯例,当现成的轮子不符合实际需求的时候,就该造新的轮子了。
上回书说到打不完的Tag会很累人,那么最基本的自动Tag功能自然是少不掉的,不过这个是作为一个设置项,需要手动开启。
PLog.init(PLogConfig.newBuilder().useAutoTag(true).build());
自动Tag是指将打印日志的位置的类名作为Tag来打印日志。
如果你只是希望观察某个地方是否被执行过,那么你可能会喜欢空消息打印的操作:
PLog.empty();
当然,如果你喜欢使用Log.INFO
以上的日志等级,你也可以在设置时对所有的空消息日志等级做一次调整。
在Android Monitor里可以看到,Logcat工具中能让我们做筛选功能的维度有:
- Log Tag
- Log Message
- Package Name
- PID
- Log Level
其中,
- Package Name和PID对于日志库的意义不大;
- Log level 应当是开发者在具体调用的时候选择,而且取值有限;
- Log message 应该尽可能保持调用时的参数原貌,不能过分侵入修改。
因此在经过思考后,我为PLog引入了Global Tag
和 Category
的概念,其本质仍然是利用tag来扩展日志的筛选维度。其中,Global
Tag是静态的(相对于应用本身),用于区分开发者自己的日志与Android Framework等其他进程内日志。而Category则是动态的,可以将应用内不同开发者的日志做分组操作,便于筛选。
使用方法:
//Init config using global tag
PLog.init(PLogConfig.newBuilder().globalTag("MyApp")
.forceConcatGlobalTag(true).build());
PLog.d("test"); //sample output: D/MyApp: test
PLog.tag("explicitTag").d(test);
//sample output: D/MyApp-explicitTag: test
PLog.tag("MainActivity").category("Alex").d("test").execute();
//sample output: D/MyApp-MainActivity[Category:Alex]: test
Category 仅在 PLog 2.x 版本上可用。
打日志时往往需要观察各种类型的参数的值,这些类型可能是int、boolean,也可能是ArrayList、HashMap,甚至可以是JSONObject或其他POJO 类对象。而且不同参数之间的拼接也很成问题,不知道你是否写过这样的代码:
Log.d("tag", "compute finished, x=" + x + ", y=" + y + ", distance=" + distance);
很熟悉吧?有了PLog,就不需要再去写这些麻烦的引号和加号了:
PLog.d("x=%d, y=%d, distance=%.2f", x, y, distance);
//Or:
PLog.objects(x, y, distance);
有没有很方便?
- 上面的例子里已经展示了在
v/d/i/w/e(msg, obj...)
方法中传递格式化参数的方案,因为底层实现是基于String.format()
,所以这里可以接受符合Java格式化规则的任何符号。 - 基于保持模块相对独立和依赖包体积的考虑,如果你需要格式化操作 POJO 和 JSON 对象,你需要引入
plog-formatter
依赖包,此外无需做任何操作。
虽然我不喜欢把日志输出绘制得很复杂,但是如果你需要,也可以在不改动PLog源码的情况下实现线框等图案。
只要自己定义一个Style接口并应用到相应的Printer即可:
Style borderStyle = new Style() {
String LINE = "------------------------------------------------";
/**
* Prefix of each log.
* @return can be null
*/
@Nullable
String msgPrefix() {
return LINE + "\n";
}
/**
* Suffix of each log.
* @return can be null
*/
@Nullable
String msgSuffix() {
return "\n" + LINE;
}
/**
* the line header property is only effected when soft wrap is enabled.
* @return the string of each line header, e.g. |.
*/
@Nullable
String lineHeader() {
return "|";
}
};
printer.setStyle(borderStyle);
我们先分析一下几个相关需求:
这很简单,只需要在封装日志类里加一个取值为BuildConfig.DEBUG
的开关即可。
这也还好,需要使用两个开关,对于INFO级别以下的日志要使用逻辑与做开关。
这个就比较麻烦了,很可能需要在设置项里做一个网络日志开关,并且在网络层每个打日志的地方都要判断开关的取值~~(某些APP确实是这么做的)~~。你也可以为网络层单独再次封装一个全功能日志类。
有点头疼了吧?
PLog中引入了拦截器(interceptor
)的概念,分为全局拦截器和通道拦截器,可以很容易地解决上述问题。
/**
* Indicate whether this item of log should be intercepted.
*
* @param level print level of this log.
* @param category category of this log, if specified.
* @param msg content of this log(before formatting!).
* @return if returns true, this log won't be printed and just be ignored. Otherwise it would
* be formatted and printed as usual.
*/
@CheckResult
boolean onIntercept(@PrintLevel int level, @NonNull String tag,
@Nullable Category category, @NonNull String msg) {
//过滤所有日志
return true;
//过滤INFO级别以下日志
return level <= Log.INFO;
//过滤网络层日志(网络层日志使用了名为"network"的category)
return category.getName().equalsIgnorecase("network");
//在文件通道中过滤:只需要对相应的printer进行设置即可,其写法一样,此处从略。
}
PLog将在2.x正式版本中实现Lint功能,拟包括以下检查项:
- 使用原生Log类(推荐使用PLog或包装类)
- 使用了返回类型为LogRequest的方法但并没有调用execute()
PLog立志做最好用的日志库,2.0版本自身也是在1.x版本的实践基础上演变而来的。如果在使用上遇到任何问题,欢迎使用GitHub Issue功能交流沟通。也非常欢迎大家一起完善这个库。
GitHub项目地址: https://github.com/Muyangmin/Android-PLog