概要:使用更好的log来调试应用。
本文会不定期更新,推荐watch下项目。如果喜欢请star,如果觉得有纰漏请提交issue,如果你有更好的点子可以提交pull request。本文的示例代码主要是基于logger、LogUtils和timber进行编写的,如果想了解更多请查看他们的详细解释。我很推荐大家多多进行对比,选择适合你自己的库来使用。
本文固定连接:https://github.com/tianzhijiexian/Android-Best-Practices
本文推荐的库:https://github.com/tianzhijiexian/logger
Android中log是这么写的:
Log.d(TAG, "This is a debug log");
我觉得不爽,而且tag连空校验都没做!
logd
就能打印一切注意:我希望只要写真正有意义的内容!
回看这些需求,不合理么?很合理,我们的宗旨就是让无意义的重复代码去死,如果死不掉就交给机器来做。我们应该做那些真正需要我们做的事情,而不是像一个没思想的猿猴一般整天写模板式代码。这才是程序员思维,而不是程序猿思维!
无论一个第三方库有多好,我还是推荐不要直接使用它,因为你很有可能会去替换这个第三方库,而且还可能会有各种意想不到的需求。对于网络请求、图片请求和log,是应该事先考虑到后续的扩展和替换的。
这个包装类用来包裹logger(logger是本文介绍的一个log库),下面是一个代码片段:
public static void d(@Nullable String info, Object... args) {
if (!mIsOpen) { // 如果把开关关闭了,那么就不进行打印
return;
}
Logger.d(info, args);
}
对于包装类的起名最好不要和“Log”这个名字类似,能有明显的区别最好,一是防止自己手抖写错了,二是方便review的时候方便自己检查有没有误用原始的Log。
现在索性把当前类名作为这样一个TAG的标识。我们可以通过下面代码来设置tag:
private static String getClassName() {
String result;
// 这里的数组的index2是根据你工具类的层级做不同的定义,这里仅仅是关键代码
StackTraceElement thisMethodStack = (new Exception()).getStackTrace()[2];
result = thisMethodStack.getClassName();
int lastIndex = result.lastIndexOf(".");
result = result.substring(lastIndex + 1, result.length());
return result;
}
这样我们就轻易的摆脱了tag的纠缠。
这个方法来自于豪哥的建议,这里感谢豪哥的意见。
我们想要偶尔打打log的tag方便做其他的处理:
public static void d(@NonNull String tag, String info, Object... args) {
Logger.t(tag).d(info, args);
}
我上面的做法是把tag用getSimpleName的方式来得到,但会因为混淆的问题在混淆的包里出现a.b.c这样的类名。如果你的log是要出现在混淆的包里的,强烈建议去手动设置tag值,否则你完全就没办法过滤了。至于如何手动设置tag的值,下面会讲到logt
这个快捷命令。
有人说我们IDE不都有代码提示了么,你还想怎么简化log的输入呢?这里可以利用as的一个模板提示的功能:
我们可以模仿这里原有的模板来做自己的代码模板,简化模板式代码的输入。至于具体模仿的方式我就不手把手教了,简单到爆。下面仅展示下自带的log模板的使用方式:
写tag:
自动填写参数和方法名:
我要美,要直观,要够酷!做到这点也简单,就是在输出前做点字符串拼接的工作,比如加上下面这行横线。
private static final String BOTTOM_BORDER = "╚═══════════════════════════";
因为做了很多拼接的工作,所以好看的log也是消耗性能的。我的习惯是调试完毕后立刻删除无用的log,这样既能减少性能影响,也减少同事的阅读代码的负担,效果如下:
这个功能其实ide是原生支持的,不相信的话你随便用原生的log打印出onCreate: (MainActivity.java:39)
试试。只不过我们可以通过一些神奇的方法来做到更好的效果:
private static String callMethodAndLine() {
String result = "at ";
StackTraceElement thisMethodStack = (new Exception()).getStackTrace()[1];
result += thisMethodStack.getClassName()+ "."; // 当前的类名(全名)
result += thisMethodStack.getMethodName();
result += "(" + thisMethodStack.getFileName();
result += ":" + thisMethodStack.getLineNumber() + ") ";
return result;
}
这里同样需要注意的是在混淆后是得不到正确的类名的,所以可以酌情让activity、fragment、view不被混淆,具体方案还是看自己的取舍。
这个需求实现起来也比较容易,如果是简单的POJO的对象,我们可用反射得到对象的类变量,通过字符串拼接的方式最终输出值。如果是map等数组结构,那么就用其内部的遍历依次输出值和内容。如果是json的字符串,就需要判断json的{}
,[]
这样的特殊字符进行换行处理。至于具体的代码是怎样了,大家移步去看源码就好,这个不是重点。重点是结果:
区分release和debug版本有系统自带的BuildConfig.DEBUG变量,用这个就可以控制是否显示log了。强制开关也很简单,在log初始化的最后判断强制开关是否打开,如果打开那么就覆盖之前的显示设置,直接显示log。转为代码就是这样:
public class BaseApplication extends Application {
// 定义是否是强制显示log的模式
protected static final boolean LOG = false;
@Override
public void onCreate() {
Logger.initialize(
Settings.getInstance()
.setLogPriority(BuildConfig.DEBUG ? Log.VERBOSE : Log.ASSERT)
);
// 如果是强制显示log,那么无论在什么模式下都显示log
if (LOG) {
Logger.setLogPriority(Log.VERBOSE)
}
}
}
有时候网络的返回值是很长的,android.util.Log
类是有最大长度限制的,为了解决这个问题。我们只需要判断这个字符串的长度,然后手动让其换行即可。
private static final int CHUNK_SIZE = 4000;
if (length <= CHUNK_SIZE) {
logContent(logType, tag, msg);
} else {
for (int i = 0; i < length; i += CHUNK_SIZE) {
int count = Math.min(length - i, CHUNK_SIZE);
//create a new String with system's default charset (which is UTF-8 for Android)
logContent(logType, tag, new String(bytes, i, count));
}
}
多参数log信息应该这样打印,避免拼接好后再打印。这样在关闭log后就不会进行字符串的拼接工作了,减少log语句在release版本中的性能影响。
Logger.d("test %s%s", "v", 5); // test v5
这条来自朋友helder的建议,感谢!
虽然提出了上面的思路和方案,但我并不能确保可以满足所有的需求,我给出下面的思维流程,方便大家随机应变:
说明:
1. 尽量用as的debug模式下的log系统,无入侵。不用写代码就能打log,十分方便。(下文会介绍)
2. 如果真的要打log做调试,先放在debug和error级别,提交代码时务必记得清除。
3. 如果提交的代码中需要在某个关键点打log,或者要给同事看这些log,可以放在在info级别以上。
4. 在realse中推荐用自己的log包装类的开关做处理,这样方便在公司内部测试时可以查看到log。
5. 如果一些信息需要在发出去的用户版本中出现,优先考虑数据统计的方式进行关键点的数据打点。
6. 如果真的要在正式发布的apk中还带着log,只保留info级别以上的,不把info级别之下的信息漏出去。
上文中我就提到了可以利用as的调试模式来加速debug,下面分享下两个和log有关的经验。
测试代码:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private int index = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(v-> {
index = 123;
Log.d(TAG, "onClick: index = " + index);
index++;
}
);
}
}
通过console热部署打印log信息
我通过debug工具,可以在任意位置打印出任意对象的值,通过这种方式就可以精准调试一些信息了。这回我是让其在不中断运行的情况下打印index的值。
动态设置值
有时候某种分支需要在某个情况下才能走到,我可以利用debug的setValue(F12)方法动态设置值,比如我把下面的123改成了520,最终在终端打印出的信息也会变成520。整个过程对原本代码完全屏蔽,无入侵。
依赖库:
https://github.com/tianzhijiexian/logger
如果你觉得这个库不好,请提交issue,万不可冷嘲热讽。要知道精品永远是个位数,而中庸的东西永远是层出不穷的。我希望大家多提意见齐心协力优化出一个精品,而不是花时间去在平庸的选项中做着选择难题。
我们可以看到即使一行代码的log都有很多点是可优化的,还明白了我们之前一直写的模板式代码是多么的枯燥乏味。
通过这篇文章,希望大家可以看到一个优化编码的思维过程,也希望大家去尝试下logger这个库。当然,我知道还是有很多人不喜欢,那么不妨提出更好的解决方案来一起讨论,不满意可以提嘛。
宁信书则不如无书,具体如何使用还得看自己的需求,欢迎通过邮件或者是gitter的方式进行交流。
在文章后面我也给出了通过idea的debug模式下打印log的方法,意思是即使你有了这个log库,但是我仍旧希望你可以能找到更好的方法来达到目的,拥有技巧,使用技巧,最终化为无形才是最高境界。相信我们的最终目的是一致的,那就是让开发越来越简便,越来越优雅~
最后说下我没直接用文章开头那几个库的原因,logger的库很漂亮,但是冗余行数过多,调试多行的数据就会受到信息干扰,timber的本身设计就是一个log的框架,打印是交给开发者自定义的。所以我将timber的框架和logger的美观实现进行了结合。这当然还要感谢logUtils的作者,让log支持了object类型。
http://ihongqiqu.com/blog/2014/10/16/android-log/
https://github.com/pengwei1024/LogUtils
https://github.com/orhanobut/logger
http://droidyue.com/blog/2015/11/01/thinking-about-android-log/
https://github.com/JakeWharton/timber
[email protected]
@天之界线2010