安卓开发打印 Log 最佳实践

发表于2016-04-03
评论0 1.2k浏览

想免费获取内部独家PPT资料库?观看行业大牛直播?点击加入腾讯游戏学院游戏程序行业精英群

711501594
  Log最佳实践
  概要:使用更好的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连空校验都没做!

二、需求
 · 我才不要每次打log都去想tag叫什么名字呢
 · 我希望可以自动把当前类名作为默认的tag
 · 如果我真的要写tag了,你就必须显示我定义的tag
 · 我希望我写的模板代码越少越好,一个logd就能打印一切
 · 我要我的log变的好看,直观,就是美
 · 我打出的log后面要根上这个log的文件源头的连接,我可以直接点击跳转到log的位置
 · log中还能提示我当前的线程名,方便我调试
 · 我还要打印出list,map,json,pojo这样的对象
 · release包中不能泄漏我高傲的log
 · 只要我想让我的log显示,release版本也阻挡不了我
 · log信息过长后应该要自动换行,我不允许我的log打印不全
 · 在release版本中残留的log代码应该对效率无影响
  注意:我希望只要写真正有意义的内容!
  回看这些需求,不合理么?很合理,我们的宗旨就是让无意义的重复代码去死,如果死不掉就交给机器来做。我们应该做那些真正需要我们做的事情,而不是像一个没思想的猿猴一般整天写模板式代码。这才是程序员思维,而不是程序猿思维!

三、实现
  无论一个第三方库有多好,我还是推荐不要直接使用它,因为你很有可能会去替换这个第三方库,而且还可能会有各种意想不到的需求。对于网络请求、图片请求和log,是应该事先考虑到后续的扩展和替换的。
1、建立包装类
  这个包装类用来包裹logger(logger是本文介绍的一个log库),下面是一个代码片段:
public static void d(@Nullable String info, Object... args) {
    if (!mIsOpen) { // 如果把开关关闭了,那么就不进行打印
        return;
    }
    Logger.d(info, args);
}
  对于包装类的起名最好不要和“Log”这个名字类似,能有明显的区别最好,一是防止自己手抖写错了,二是方便review的时候方便自己检查有没有误用原始的Log。
2、自动打tag
  现在索性把当前类名作为这样一个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的纠缠。
  这个方法来自于豪哥的建议,这里感谢豪哥的意见。
3、自定义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这个快捷命令。
4、将Log代码快捷模板
  有人说我们IDE不都有代码提示了么,你还想怎么简化log的输入呢?这里可以利用as的一个模板提示的功能:

  我们可以模仿这里原有的模板来做自己的代码模板,简化模板式代码的输入。至于具体模仿的方式我就不手把手教了,简单到爆。下面仅展示下自带的log模板的使用方式:
  写tag:


  自动填写参数和方法名:


5、让log更加美观
  我要美,要直观,要够酷!做到这点也简单,就是在输出前做点字符串拼接的工作,比如加上下面这行横线。
private static final String BOTTOM_BORDER = "╚═══════════════════════════";
  因为做了很多拼接的工作,所以好看的log也是消耗性能的。我的习惯是调试完毕后立刻删除无用的log,这样既能减少性能影响,也减少同事的阅读代码的负担,效果如下:


6、显示当前方法名和所在类并加超链
  这个功能其实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不被混淆,具体方案还是看自己的取舍。
7、支持POJO、Map、Collection、jsonStr、Array
  这个需求实现起来也比较容易,如果是简单的POJO的对象,我们可用反射得到对象的类变量,通过字符串拼接的方式最终输出值。如果是map等数组结构,那么就用其内部的遍历依次输出值和内容。如果是json的字符串,就需要判断json的{},[]这样的特殊字符进行换行处理。至于具体的代码是怎样了,大家移步去看源码就好,这个不是重点。重点是结果:


8、增加自动化或强制开关
  区分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)
        }
    }
}
9、支持超长的log信息
  有时候网络的返回值是很长的,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));
    }
}
10、解决log字符拼接的效率影响
  多参数log信息应该这样打印,避免拼接好后再打印。这样在关闭log后就不会进行字符串的拼接工作了,减少log语句在release版本中的性能影响。
Logger.d("test %s%s", "v", 5); // test v5
  这条来自朋友helder的建议,感谢!
11、各种需求和应对方案
  虽然提出了上面的思路和方案,但我并不能确保可以满足所有的需求,我给出下面的思维流程,方便大家随机应变:
  Created with Raphaël 2.1.2优先用ida的debug系统实在不行就在debug和error级别打log信息不够时才在info、warning、error的级别打提交测试时,删除无用log,预留测试环境中的log开关正式版中优先用数据统计平台或应用打点工具来代替log日志如果必须出现log,正式版中仅泄漏info级别以上的log,即warn和erro
  说明:
 · 尽量用as的debug模式下的log系统,无入侵。不用写代码就能打log,十分方便。(下文会介绍)
 · 如果真的要打log做调试,先放在debug和error级别,提交代码时务必记得清除。
 · 如果提交的代码中需要在某个关键点打log,或者要给同事看这些log,可以放在在info级别以上。
 · 在realse中推荐用自己的log包装类的开关做处理,这样方便在公司内部测试时可以查看到log。
 · 如果一些信息需要在发出去的用户版本中出现,优先考虑数据统计的方式进行关键点的数据打点。
 · 如果真的要在正式发布的apk中还带着log,只保留info级别以上的,不把info级别之下的信息漏出去。

四、IDEA的超强debug技巧
  上文中我就提到了可以利用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++;
            }
        );
    }
1、通过console热部署打印log信息
  我通过debug工具,可以在任意位置打印出任意对象的值,通过这种方式就可以精准调试一些信息了。这回我是让其在不中断运行的情况下打印index的值。


2、动态设置值
  有时候某种分支需要在某个情况下才能走到,我可以利用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

  加入GAD的核心用户QQ群:484290331,各类活动奖励任你拿,最新资讯任你读,众多教学任你免费学,如此好地方赶紧加入吧!另VR专属群:476511561,专业VR技术分享,专业导师指导为你答疑解惑,大型小型活动奖励等你拿,免费学习赚奖励的天地,欢迎你加入哟!

原文链接

著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

游戏学院公众号二维码
腾讯游戏学院
微信公众号

提供更专业的游戏知识学习平台