Android开发Log最佳实践-一个简单、漂亮、功能强大的Android日志程序:logger

时间:2021-03-21 19:47:03

Log最佳实践

概要:使用更好的log来调试应用。 
本文会不定期更新,推荐watch下项目。如果喜欢请star,如果觉得有纰漏请提交issue,如果你有更好的点子可以提交pull request。本文的示例代码主要是基于loggerLogUtilstimber进行编写的,如果想了解更多请查看他们的详细解释。我很推荐大家多多进行对比,选择适合你自己的库来使用。

本文固定连接:https://github.com/tianzhijiexian/Android-Best-Practices

本文推荐的库:https://github.com/tianzhijiexian/logger

一、需求背景

Android中log是这么写的:

 
 
  1. Log.d(TAG, "This is a debug log");

我觉得不爽,而且tag连空校验都没做!

二、需求

  1. 我才不要每次打log都去想tag叫什么名字呢
  2. 我希望可以自动把当前类名作为默认的tag
  3. 如果我真的要写tag了,你就必须显示我定义的tag
  4. 我希望我写的模板代码越少越好,一个logd就能打印一切
  5. 我要我的log变的好看,直观,就是美
  6. 我打出的log后面要根上这个log的文件源头的连接,我可以直接点击跳转到log的位置
  7. log中还能提示我当前的线程名,方便我调试
  8. 我还要打印出list,map,json,pojo这样的对象
  9. release包中不能泄漏我高傲的log
  10. 只要我想让我的log显示,release版本也阻挡不了我
  11. log信息过长后应该要自动换行,我不允许我的log打印不全
  12. 在release版本中残留的log代码应该对效率无影响

注意:我希望只要写真正有意义的内容!

回看这些需求,不合理么?很合理,我们的宗旨就是让无意义的重复代码去死,如果死不掉就交给机器来做。我们应该做那些真正需要我们做的事情,而不是像一个没思想的猿猴一般整天写模板式代码。这才是程序员思维,而不是程序猿思维!

三、实现

无论一个第三方库有多好,我还是推荐不要直接使用它,因为你很有可能会去替换这个第三方库,而且还可能会有各种意想不到的需求。对于网络请求、图片请求和log,是应该事先考虑到后续的扩展和替换的。

建立包装类

这个包装类用来包裹logger(logger是本文介绍的一个log库),下面是一个代码片段:

 
 
  1. public static void d(@Nullable String info, Object... args) {
  2. if (!mIsOpen) { // 如果把开关关闭了,那么就不进行打印
  3. return;
  4. }
  5. Logger.d(info, args);
  6. }

对于包装类的起名最好不要和“Log”这个名字类似,能有明显的区别最好,一是防止自己手抖写错了,二是方便review的时候方便自己检查有没有误用原始的Log。

自动打tag

现在索性把当前类名作为这样一个TAG的标识。我们可以通过下面代码来设置tag:

 
 
  1. private static String getClassName() {
  2. String result;
  3. // 这里的数组的index2是根据你工具类的层级做不同的定义,这里仅仅是关键代码
  4. StackTraceElement thisMethodStack = (new Exception()).getStackTrace()[2];
  5. result = thisMethodStack.getClassName();
  6. int lastIndex = result.lastIndexOf(".");
  7. result = result.substring(lastIndex + 1, result.length());
  8. return result;
  9. }

这样我们就轻易的摆脱了tag的纠缠。

这个方法来自于豪哥的建议,这里感谢豪哥的意见。

自定义tag

我们想要偶尔打打log的tag方便做其他的处理:

 
 
  1. public static void d(@NonNull String tag, String info, Object... args) {
  2. Logger.t(tag).d(info, args);
  3. }

我上面的做法是把tag用getSimpleName的方式来得到,但会因为混淆的问题在混淆的包里出现a.b.c这样的类名。如果你的log是要出现在混淆的包里的,强烈建议去手动设置tag值,否则你完全就没办法过滤了。至于如何手动设置tag的值,下面会讲到logt这个快捷命令。

将Log代码快捷模板

有人说我们IDE不都有代码提示了么,你还想怎么简化log的输入呢?这里可以利用as的一个模板提示的功能: 
Android开发Log最佳实践-一个简单、漂亮、功能强大的Android日志程序:logger

我们可以模仿这里原有的模板来做自己的代码模板,简化模板式代码的输入。至于具体模仿的方式我就不手把手教了,简单到爆。下面仅展示下自带的log模板的使用方式: 
写tag: 
Android开发Log最佳实践-一个简单、漂亮、功能强大的Android日志程序:logger 
自动填写参数和方法名: 
Android开发Log最佳实践-一个简单、漂亮、功能强大的Android日志程序:logger

让log更加美观

我要美,要直观,要够酷!做到这点也简单,就是在输出前做点字符串拼接的工作,比如加上下面这行横线。

 
 
  1. private static final String BOTTOM_BORDER = "╚═══════════════════════════";

因为做了很多拼接的工作,所以好看的log也是消耗性能的。我的习惯是调试完毕后立刻删除无用的log,这样既能减少性能影响,也减少同事的阅读代码的负担,效果如下: 
Android开发Log最佳实践-一个简单、漂亮、功能强大的Android日志程序:logger

显示当前方法名和所在类并加超链

这个功能其实ide是原生支持的,不相信的话你随便用原生的log打印出onCreate: (MainActivity.java:39)试试。只不过我们可以通过一些神奇的方法来做到更好的效果:

 
 
  1. private static String callMethodAndLine() {
  2. String result = "at ";
  3. StackTraceElement thisMethodStack = (new Exception()).getStackTrace()[1];
  4. result += thisMethodStack.getClassName()+ "."; // 当前的类名(全名)
  5. result += thisMethodStack.getMethodName();
  6. result += "(" + thisMethodStack.getFileName();
  7. result += ":" + thisMethodStack.getLineNumber() + ") ";
  8. return result;
  9. }

这里同样需要注意的是在混淆后是得不到正确的类名的,所以可以酌情让activity、fragment、view不被混淆,具体方案还是看自己的取舍。

支持POJO、Map、Collection、jsonStr、Array

这个需求实现起来也比较容易,如果是简单的POJO的对象,我们可用反射得到对象的类变量,通过字符串拼接的方式最终输出值。如果是map等数组结构,那么就用其内部的遍历依次输出值和内容。如果是json的字符串,就需要判断json的{},[]这样的特殊字符进行换行处理。至于具体的代码是怎样了,大家移步去看源码就好,这个不是重点。重点是结果: 
Android开发Log最佳实践-一个简单、漂亮、功能强大的Android日志程序:logger

增加自动化或强制开关

区分release和debug版本有系统自带的BuildConfig.DEBUG变量,用这个就可以控制是否显示log了。强制开关也很简单,在log初始化的最后判断强制开关是否打开,如果打开那么就覆盖之前的显示设置,直接显示log。转为代码就是这样:

 
 
  1. public class BaseApplication extends Application {
  2. // 定义是否是强制显示log的模式
  3. protected static final boolean LOG = false;
  4. @Override
  5. public void onCreate() {
  6. Logger.initialize(
  7. Settings.getInstance()
  8. .setLogPriority(BuildConfig.DEBUG ? Log.VERBOSE : Log.ASSERT)
  9. );
  10. // 如果是强制显示log,那么无论在什么模式下都显示log
  11. if (LOG) {
  12. Logger.setLogPriority(Log.VERBOSE)
  13. }
  14. }
  15. }

支持超长的log信息

有时候网络的返回值是很长的,android.util.Log类是有最大长度限制的,为了解决这个问题。我们只需要判断这个字符串的长度,然后手动让其换行即可。

 
 
  1. private static final int CHUNK_SIZE = 4000;
  2. if (length <= CHUNK_SIZE) {
  3. logContent(logType, tag, msg);
  4. } else {
  5. for (int i = 0; i < length; i += CHUNK_SIZE) {
  6. int count = Math.min(length - i, CHUNK_SIZE);
  7. //create a new String with system's default charset (which is UTF-8 for Android)
  8. logContent(logType, tag, new String(bytes, i, count));
  9. }
  10. }

解决log字符拼接的效率影响

多参数log信息应该这样打印,避免拼接好后再打印。这样在关闭log后就不会进行字符串的拼接工作了,减少log语句在release版本中的性能影响。

 
 
  1. Logger.d("test %s%s", "v", 5); // test v5

这条来自朋友helder的建议,感谢!

各种需求和应对方案

虽然提出了上面的思路和方案,但我并不能确保可以满足所有的需求,我给出下面的思维流程,方便大家随机应变:

优先用ida的debug系统实在不行就在debug和error级别打log信息不够时才在info、warning、error的级别打提交测试时,删除无用log,预留测试环境中的log开关正式版中优先用数据统计平台或应用打点工具来代替log日志如果必须出现log,正式版中仅泄漏info级别以上的log,即warn和erro

说明: 
1. 尽量用as的debug模式下的log系统,无入侵。不用写代码就能打log,十分方便。(下文会介绍) 
2. 如果真的要打log做调试,先放在debug和error级别,提交代码时务必记得清除。 
3. 如果提交的代码中需要在某个关键点打log,或者要给同事看这些log,可以放在在info级别以上。 
4. 在realse中推荐用自己的log包装类的开关做处理,这样方便在公司内部测试时可以查看到log。 
5. 如果一些信息需要在发出去的用户版本中出现,优先考虑数据统计的方式进行关键点的数据打点。 
6. 如果真的要在正式发布的apk中还带着log,只保留info级别以上的,不把info级别之下的信息漏出去。

四、IDEA的超强debug技巧

上文中我就提到了可以利用as的调试模式来加速debug,下面分享下两个和log有关的经验。 
测试代码:

 
 
  1. public class MainActivity extends AppCompatActivity {
  2. private static final String TAG = "MainActivity";
  3. private int index = 0;
  4. @Override
  5. protected void onCreate(Bundle savedInstanceState) {
  6. super.onCreate(savedInstanceState);
  7. Button button = (Button) findViewById(R.id.button);
  8. button.setOnClickListener(v-> {
  9. index = 123;
  10. Log.d(TAG, "onClick: index = " + index);
  11. index++;
  12. }
  13. );
  1. 通过console热部署打印log信息 
    我通过debug工具,可以在任意位置打印出任意对象的值,通过这种方式就可以精准调试一些信息了。这回我是让其在不中断运行的情况下打印index的值。 
    Android开发Log最佳实践-一个简单、漂亮、功能强大的Android日志程序:logger

  2. 动态设置值 
    有时候某种分支需要在某个情况下才能走到,我可以利用debug的setValue(F12)方法动态设置值,比如我把下面的123改成了520,最终在终端打印出的信息也会变成520。整个过程对原本代码完全屏蔽,无入侵。 
    Android开发Log最佳实践-一个简单、漂亮、功能强大的Android日志程序:logger

五、最终成果

依赖库: 
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

作者

Android开发Log最佳实践-一个简单、漂亮、功能强大的Android日志程序:logger

developer_kale@foxmail.com 
@天之界线2010