概述
项目开发过程中,我们经常通过日志来排查问题,日志俨然已经成为了我们工作中不可缺少的一部分。 设计一套好用的合理的Log日志打印框架,也是考验程序员功底的方式之一。
日志开关的设定
大部分情况下,我们只需要在debug包中开启日志打印,线上包 如果开启了日志打印,则存在 信息安全风险,如果存在又长又大的日志,还有可能影响app性能。
BuildConfig.DEBUG
在每个module编译之后,都会生成这么一个 BuildConfig.java文件。
他表示当前运行的app是不是debug版本。如果是debug版本,那么就打印日志。
java
import android.util.Log;
public class LogUtil {
private boolean logSwitch = BuildConfig.DEBUG;
private void d(String tag, String msg) {
if (logSwitch) {
Log.d(tag, msg);
}
}
}
但是想的美好,总是会出现意外。如果一个release版本的app在 内测的时候出现问题,按照上面的方式,我们还必须 给他打一个 debug版本的apk。 仔细想一下这一过程中我们需要做什么。
- 确定他所用的app版本
- 代码仓库上找到 他所用版本的提交节点(tag,或者branch的commit记录)
- 本地代码切换到 这个提交节点
- 打出一个debug包
- 安装到他的手机上
- 安装完成之后,app会重启
- 尝试复现问题,查看日志排查问题
等着一切顺利完成,半个小时过去了。如果不顺利,半天就过去了,黄花菜都凉了。这种效率,在高速迭代的App开发领域是不合格的。
所以我们还需要另外的方式在不影响release包运行性能以及安全性的前提下,开启日志打印。
System properties
Windows系统,mac系统,linux系统。只要是操作系统,就有 系统属性的概念,或者称为系统环境变量,各操作系统经常把重要的参数,通过键值对的方式放置在 环境变量表中。
上图中的System.getProperty()
方法就是 获取安卓系统中某个环境变量的写法。
为了快速查看日志,我们将 修改日志框架如下,用本地的BuildConfig.DEBUG 和 系统环境变量中的 com.example.gradleprotest.log
的值进行综合判断,如果当前是DEBUG包,或者 com.example.gradleprotest.log
属性值为 true,就认为可以打印日志。
java
public class LogUtil {
private boolean getLogSwitch() {
boolean logSwitch = BuildConfig.DEBUG;
String aFalse = System.getProperty("com.example.gradleprotest.log", "false");
return logSwitch || "true".equals(aFalse);
}
private void d(String tag, String msg) {
if (getLogSwitch()) {
Log.d(tag, msg);
}
}
}
需要注意的是,在没有去设置com.example.gradleprotest.log
这个环境变量时,去获取它,上面得到的结果是 false。
在默认情况下,日志开关是关闭的,当我们需要现场排查问题时,则打开开关。当我们排查完毕时,则需要关闭开关。所以上面的代码我们继续改成如下:
java
public class LogUtil {
private static final String LOG_SWITCH_SYS_NAME = "com.example.gradleprotest.log";
/**
* 获得日志开关
*
* @return
*/
private boolean getLogSwitch() {
boolean logSwitch = BuildConfig.DEBUG;
String aFalse = System.getProperty(LOG_SWITCH_SYS_NAME, "false");
return logSwitch || "true".equals(aFalse);
}
/**
* 打开日志开关
*/
public void openLogSwitch() {
System.setProperty(LOG_SWITCH_SYS_NAME, "true");
}
/**
* 打开日志开关
*/
public void closeLogSwitch() {
System.setProperty(LOG_SWITCH_SYS_NAME, "false");
}
private void d(String tag, String msg) {
if (getLogSwitch()) {
Log.d(tag, msg);
}
}
}
定义合理的打开日志开关的方式
上面的代码解决了现场排查问题流程很长的问题,但是我们仍需要一个周全的方案打开此开关。这个环节中需要考虑的是,日志一旦打开,打印出来的依然是明文,尤其是敏感信息,依然有泄漏风险。
以下是几个方式:
- 通过某个隐秘的入口,连续敲击的方式。其实这就是 安卓手机打开开发者调试模式的方法,通常多次点击按照系统版本号,来开启开发者模式。
- 如果app接入了推送,那我们可以通过推送后台,向app发送一条特定的透传消息(无需显示在通知栏中),app收到之后,按照透传分发的逻辑,执行 openLogSwitch 或者 closeLogSwitch 的方法。
特别注意混淆的问题
assumenosideeffects
,是 安卓的混淆规则中的一个关键字。它的作用是,指定某些特定的方法或类不会产生副作用(side effects),并允许 ProGuard 在优化代码时忽略这些副作用。
在 Java 中,我们通常写代码来执行某些任务,比如计算数字、获取数据等。有些代码只是执行任务,并返回结果,而不会对其他东西产生任何影响,比如修改变量的值或进行输入输出操作。
然而,在代码混淆和优化的过程中,一些看起来不重要的方法或类可能会被误认为没有对程序的其他部分产生影响 ,因此被删除或优化掉。这可能会导致应用程序出现错误或不可靠的行为。
就好比我们这个LogUtil类,它有一个openLogSwitch方法用于启用日志记录。当我们调用该方法时,我们希望日志记录已经启用。
但是,由于代码优化或误解,这个启用日志记录的方法可能被删除,因为它看起来没有直接的结果或影响。结果就是日志记录没有被启用,我们写入的信息丢失了。
为了避免这种问题,我们可以使用 assumenosideeffects
选项来告诉代码混淆工具,即使这个方法看起来没有明显的结果,它实际上有一些重要的影响,不能被删除或优化掉。
说一句人话 吧,安卓混淆代码
的过程,可能对一些多你一个不多,少你一个不少 的代码进行优化删除, 也就是说,我们看上去打开了 日志开关,其实这句代码并没有打到apk中。
要避免此问题,我们需要在混淆规则中加入如下代码:
java
-assumenosideeffects class LogUtil {
public void openLogSwitch();
public void closeLogSwitch();
}
日志的保存
有些情况下,问题或者bug无法复现,我们就无法查看现场,但是可以通过查看本地日志文件的方式来继续排查。
这就要求我们的日志框架能够保存 关键日志到本地文件中。
我们可以在app运行时触发某些特殊情况时,直接保存关键日志到 app私有目录的文件中。
由于牵涉到文件的写操作,为了不影响UI线程,我们必须开启子线程。这时候最好的方式就是启用 线程池。比如 单线程的线程池,Executors.newSingleThreadExecutor()
如下图:
Log框架参数设置
一个日志框架,可能有多个配置项,比如
- 日志开关,
- 日志全局tag,
- 文件读写开关,
- 日志文件存储目录,
- 日志打印级别,
- 日志过滤策略。
这些都属于配置性质的参数,我们应该将它集中到一个类中去维护,再将整个config对象传递给LogUtil
特殊日志格式转化
日志中,经常需要打印一些XML,JSON,或者其他一些奇奇怪怪格式的内容,此时,如果任由它无限长的去打印在一行中,无非就是我们自己给自己挖坑,排查的时候,从头到尾去拷贝你就知道有多痛苦了。
所以,所以,要尝试对打印的内容进行 格式判定,如果是json就按照json的缩进格式输出。XML同理。
借助 日志打印的第三方库
推荐XLog,它是腾讯推出的MARS框架中的一个小组件,它包含了 上面提到的所有特性,并且兼具了信息安全性,并且更加友好和全面。
初始化:
调用日志打印:
打印结果: