Log最佳实践

作者: 天之界线2010 | 来源:发表于2016-07-05 09:35 被阅读3089次

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

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


    一、背景

    Android中的log是这么写的:

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

    android.util.Log类做的事情很简单,符合kiss原则,但是随着业务的不断发展,logcat中就会有多个部门的各种log,不同手机系统自己的一些log也会参杂进来,逼迫我们要扩展log类。

    二、需求

    1. 我才不要每次打log都去想tag叫什么名字呢
    2. 通常情况下请自动把当前类名作为默认的tag,但也允许我自由指定
    3. 我希望我写的模板式代码越少越好,一个logd就能打印一切
    4. 我要打印出list,map,json,pojo这样的对象
    5. 我的log绝对不要和其余的杂乱log混在一起
    6. log信息过长后应该要自动换行,我不允许我的log打印不全
    7. 我要我的log变的好看,直观,就是美
    8. log中还要能显示我当前的线程名,方便我调试多线程
    9. 我打出的log后面要根上这个log的地址,可以直接外链到log的位置
    10. release包中不能泄漏我高傲的log,但只要我想让它显示,release版本也阻挡不了我
    11. 在release版本中残留的log代码应该对app运行效率影响极低
    12. 它能自动将try-catch住的crash通过log上传到Crashlytics

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

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

    三、实现

    分析上述的需求后,我将其分为四类: 使用、显示和扩展。

    使用篇

    建立包装类

    无论一个第三方库有多好,我还是推荐不直接使用它,因为你很有可能会去替换这个第三方库,而且一个第三方库肯定无法满足各种奇葩需求。所以,对于网络库、图片库和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() {
        // 这里的数组的index,即2,是根据你工具类的层级取的值,可根据需求改变
        StackTraceElement thisMethodStack = (new Exception()).getStackTrace()[2]; 
        String result = thisMethodStack.getClassName();
        int lastIndex = result.lastIndexOf(".");
        result = result.substring(lastIndex + 1, result.length());
        return result;
    }
    

    这样我们就轻易的摆脱了tag的纠缠。
    需要注意的是,获取堆栈的方法是有性能消耗的,所以在主线程的log可能会引起一些卡顿,所以强烈建议在release版本中不要使用这个方法。

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

    自定义tag

    除了自动打tag外,我们肯定要让其支持自定义tag:

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

    这个d(tag, info, args...)是上面d(info, args...)的扩展,这里要注意的是tag的选取。

    常用的做法是用getSimpleName的方式来得到tag,但如果你加了混淆,很多类(Activity、View不一定会被混淆)就会被混淆为a/b/c这样的单词。因此,如果你的log要出现在混淆的包里的,我强烈建议去手动设置tag值,否则打出来的log就是很难过滤的了。

    至于如何手动设置tag的值,下面会讲到logt这个快捷命令。

    自定义全局tag和tag前缀

    如果你的项目很庞大或者采用了插件化和组件化方案,那么你肯定会涉及到多人开发的问题。底层平台是暴露统一的log接口,但是上层开发人员种类繁多,如何在繁杂的log中找到自己部门的自己关心的log呢?

    在这种情况下我们可以采用如下两种方案:

    1. 自行调试时关闭无关部门的log输出
    2. 每个部门有自定义的tag前缀

    对于方案一,我们本身的log系统底层采用的是timber,它本身就是通过“种树”的方式进行log分发的,我们只需要在我们项目的最开始调用

    Logger.uprootAll();
    // or
    Timber.uprootAll();
    

    将所有之前的log通道移除,这样就清空了无用的log了。

    相比起方案一的简单粗暴,方案二倒是温和实用的多。我们通过在logger初始化设置一个tagPrefix,这个前缀就会伴随着我们私有项目的所有log了,以后直接搜索这个前缀就可以过滤出想要的信息了。

    开启和关闭log

    有时候在调试过程中可能会要支持测试同学的动态关闭和开启log的功能。

    Logger.closeLog();
    Logger.openLog(Log.INFO);
    

    这个操作可以支持在应用运行的时的任何时候进行开关。

    将Log代码快捷模板

    有人说我们IDE不都有代码提示了么,你还想怎么简化log的输入呢?这里可以利用as的模板提示的功能:

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

    这个功能其实ide是原生支持的,不相信的话你随便用原生的log打印出onCreate: (MainActivity.java:31)试试。

    我们可以通过下面的方法来做到更好的效果:

        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不被混淆,具体方案还是看自己的取舍。

    增加当前线程的信息

    当你调试过多线程,你就会发现log中带有线程的信息是很方便的。

    Thread.currentThread().getName()
    

    Logger的尾巴上会带有线程的名字,方便大家进行调试。

    支持POJO、Map、Collection、jsonStr、Array

    这个需求实现起来也比较容易:

    • 如果是POJO,我们可用反射得到对象的类变量,通过字符串拼接的方式最终输出值
    • 如果是map等数组结构,那么就用其内部的遍历依次输出值和内容
    • 如果是json的字符串,就需要判断json的{},[]这样的特殊字符进行换行处理

    至于具体是如何实现的,大家移步去看源码就好,这个不是重点,重点是结果:

    不推荐打印每次网络请求的json,只推荐在调试某个数据的时候进行打印,否则信息太多,而且效率很低,不实用。

    自定义输出样式

    我们看到了orhanobut/loggerelvishew/xLog都十分好看,但是tianzhijiexian/logger的log看起来就没那么美观了,所以这个库支持了自定的style,让使用者可以自定义输出样式。

    PrintStyle.java

    public abstract class PrintStyle {
    
        @Nullable
        protected abstract String beforePrint();
    
        @NonNull
        protected abstract String printLog(String message, int line, int wholeLineCount);
    
        @Nullable
        protected abstract String afterPrint();
    }
    

    这个抽象类提供了三个方法,用来得到log打印前,打印时,打印后的内容,我们可以通过它来实现自定义的样式。

    **使用XLog样式后的输出: **

    PS:Logger的不美观其实是折衷的结果。美观必然会带来数据的冗余,但原始的log却又不足够清晰。Logger最终选择了一个轻量的log样式,既保证了清晰易辨认又不会带来过多的冗余信息。

    支持超长的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));
        }
    }
    

    自定义过滤规则

    当崩溃出现的时候,有时候会将我们的log清屏,大大影响了我们的调试工作。所以我们可以在合适的时候利用Edit Filter Configuration这个功能。

    Edit Filter Configuration十分强大,并且支持正则。一般情况下使用Show only selected application就搞定了,是否使用Edit Filter Configuration就看你的具体场景了。

    扩展篇

    增加自动化或强制开关

    要区分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(
                new Settings()
                        .setLogPriority(BuildConfig.DEBUG ? Log.VERBOSE : Log.ASSERT)
            );
            
            // 如果是强制显示log,那么无论在什么模式下都显示log
            if (LOG) {
                Logger.getSettings().setLogPriority(Log.VERBOSE)
            }
        }
    }
    

    以后要是需要做log的开关,那么只需要通过settings重设log级别即可:

    Logger.getSettings().setLogPriority(Log.ASSERT); // close log
    

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

    多参数log信息应该利用占位符进行打印,尽量避免手动拼接字符串。这样好处是:在关闭log后就不会进行字符串的拼接工作了,减少log语句在release版本中的性能影响。

    封装类.d("test %s%s", "v", 5); // test v5
    
    public static void d(@Nullable String info, Object... args) {
        if (!mIsOpen) { // 如果把开关关闭了,自然就不进行字符串拼接
            return;
        }
        Logger.d(info, args); // 内部会做String.format()
    }
    

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

    通过混淆剔除log代码

    如果你确定你的log代码在release版本中是无需存在的,那么我分享一个方案来帮你干掉它。

    比如你的混淆配置文件叫proguard-rules.pro,里面有如下代码:

    -assumenosideeffects class kale.log.LL { // 假设我们的log类是LL
        public static *** d(...); // public static void d(...);
        public static *** i(...);
        public static *** v(...);
    }
    

    然后在build.gradlez中启用混淆:

    buildTypes {
            release {
                minifyEnabled true
                shrinkResources true // 是否去除无效的资源文件
                // 注意是用proguard-android-optimize.txt而不是proguard-android.txt
                proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
                signingConfig signingConfigs.release
            }
        }
    

    要令assumenosideeffects生效,就需要开启混淆中的优化选项,而默认的proguard-android.txt是不会开启优化选项的。如果我们需要开启混淆的话,那么建议我们采用 proguard-android-optimize.txt。

    proguard-android-optimize的全部内容如下:

    # This is a configuration file for ProGuard.
    # http://proguard.sourceforge.net/index.html#manual/usage.html
    
    # Optimizations: If you don't want to optimize, use the
    # proguard-android.txt configuration file instead of this one, which
    # turns off the optimization flags.  Adding optimization introduces
    # certain risks, since for example not all optimizations performed by
    # ProGuard works on all versions of Dalvik.  The following flags turn
    # off various optimizations known to have issues, but the list may not
    # be complete or up to date. (The "arithmetic" optimization can be
    # used if you are only targeting Android 2.0 or later.)  Make sure you
    # test thoroughly if you go this route.
    -optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
    -optimizationpasses 5
    -allowaccessmodification
    -dontpreverify
    
    # The remainder of this file is identical to the non-optimized version
    # of the Proguard configuration file (except that the other file has
    # flags to turn off optimization).
    
    -dontusemixedcaseclassnames
    -dontskipnonpubliclibraryclasses
    -verbose
    
    -keepattributes *Annotation*
    -keep public class com.google.vending.licensing.ILicensingService
    -keep public class com.android.vending.licensing.ILicensingService
    
    # For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
    -keepclasseswithmembernames class * {
        native <methods>;
    }
    
    # keep setters in Views so that animations can still work.
    # see http://proguard.sourceforge.net/manual/examples.html#beans
    -keepclassmembers public class * extends android.view.View {
       void set*(***);
       *** get*();
    }
    
    # We want to keep methods in Activity that could be used in the XML attribute onClick
    -keepclassmembers class * extends android.app.Activity {
       public void *(android.view.View);
    }
    
    # For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
    -keepclassmembers enum * {
        public static **[] values();
        public static ** valueOf(java.lang.String);
    }
    
    -keepclassmembers class * implements android.os.Parcelable {
      public static final android.os.Parcelable$Creator CREATOR;
    }
    
    -keepclassmembers class **.R$* {
        public static <fields>;
    }
    
    # The support library contains references to newer platform versions.
    # Don't warn about those in case this app is linking against an older
    # platform version.  We know about them, and they are safe.
    -dontwarn android.support.**
    
    # Understand the @Keep support annotation.
    -keep class android.support.annotation.Keep
    
    -keep @android.support.annotation.Keep class * {*;}
    
    -keepclasseswithmembers class * {
        @android.support.annotation.Keep <methods>;
    }
    
    -keepclasseswithmembers class * {
        @android.support.annotation.Keep <fields>;
    }
    
    -keepclasseswithmembers class * {
        @android.support.annotation.Keep <init>(...);
    }
    

    上面的注释就是采用优化方案来剔除log的风险点,所以要慎重使用!!!

    这里也提到了一般推荐用proguard-android.txt来做混淆方案,如果你要是用了proguard-android-optimize.txt的话,请一定要测试充分在发布app。

    将try-catch的信息通过log上传到Crashlytics

    我们有时候为了防御某个未知原因的崩溃,经常会进行try-catch。这样虽然让其没崩溃,但是也隐藏了错误,以至于我们始终没有办法弄懂错误出现的原因。
    我希望可以通过把catch的异常通过log系统分发到崩溃分析网站上(如:Crashlytics),这样既能防御问题,又可以帮助开发者知道崩溃产生的原因,方便以后针对性的进行处理。

    代码参考自:https://blog.xmartlabs.com/2015/07/09/Android-logging-with-Crashlytics-and-Timber/

    模拟

        /**
         * 这里模拟后端给客户端传值的情况。
         *
         * 这里的id来自外部输入,如果外部输入的值有问题,那么就可能崩溃。
         * 但理论上是不会有数据异常的,为了不崩溃,这里加try-catch
         */
        private void setRes(@StringRes int resId) {
            TextView view = new TextView(this);
    
            try {
                view.setText(resId); // 如果出现了崩溃,那么就会调用崩溃处理机制
            } catch (Exception e) {
                // 防御了崩溃
                e.printStackTrace();
                
                // 把崩溃的异常和当前的上下文通过log系统分发
                Logger.e(e, "res id = " + resId);
            }
        }
    

    接下来,我们建立一个crash分发tree:

    public class CrashlyticsTree extends Timber.Tree {
    
        @Override
        protected void log(int priority, @Nullable String tag, @Nullable String message, @Nullable Throwable t) {
            if (priority == Log.VERBOSE || priority == Log.DEBUG || priority == Log.INFO) {
                // 只分发异常
                return;
            }
    
            if (t == null && message != null) {
                Crashlytics.logException(new Exception(message));
            } else if (t != null && message != null) {
                Crashlytics.logException(new Exception(message, t));
            } else if (t != null) {
                Crashlytics.logException(t);
            }
        }
    }
    
    // ---------------
    
    if (!BuildConfig.DEBUG) {    // for release    
        Logger.plant(new CrashlyticsTree()); // plant a tree
    }
    

    一旦用户发生了崩溃,我们现在就可以通过Crashlytics进行分析,这样的错误会自动归档在Crashlytics报表的non-fatals中。通过这样的方式,可以方便我们排查出真正的问题,解决后就可以真正去掉这个try-catch了。

    注意:
    因为我们有些错误是不希望上传的,有些是希望上传的,所以我建议在使用Logger.e()的时候,通过你的包装类来做个处理(加参数或加方法),让使用者明确这个log将通向何方,不希望引起理解混乱。

    增加log的扩展性

    正如上面提到的,我们的log可能需要分发到不同的系统,这也是我采用timber的原因。我们除了将线上的错误分发到崩溃统计系统外,也可能要将log保存到sd卡或是做其他的处理,所以目前logger利用timber的tree实现了分发的功能。

    Logger内部的实现:

    public static void plant(Timber.Tree tree) {
        Timber.plant(tree);
    }
    

    关于如何plant可以参考下Timber的具体代码。

    通过自定义lint来规范log

    大多数团队会定义自己的log类来进行log的打印,我们最好可以通过自定义的lint来在代码编写时防止开发者错用log类。

    详细的内容可以参考:《Android自定义Lint实践》

    利用IDEA的debug工具打log

    上文中我就提到了可以利用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.通过断点来代替log
    如果你的目的仅仅是进行调试,那么以我的经验来看,我推荐通过断点进行操作而不是log。

    断点的好处是随时热部署不用重新跑一次代码,而且完全无入侵。如果你要调试的是再某个场景下的情况,你也可以通过条件断点进行处理:

    2.通过console热部署打印log信息
    我通过debug工具,可以在任意位置打印出任意对象的值,通过这种方式就可以精准调试一些信息了。
    下图是我让其在不中断运行的情况下打印index的值。

    四、总结

    我们可以看到即使一行代码的log都有很多点是可优化的,还明白了我们之前一直写的模板式代码是多么的枯燥乏味。
    通过这篇文章,希望大家可以看到一个优化编码的思维过程,也希望大家去尝试下logger这个库。当然,我知道还是有很多人不喜欢,那么不妨提出更好的解决方案来一起讨论,不满意可以提issue。
    要知道精品永远是个位数,而中庸的东西永远是层出不穷的。我希望大家多提意见齐心协力优化出一个精品,而不是花时间去在平庸的选项中做着选择难题。

    五、尾声

    在文章中我给出了通过idea的debug模式下打印log的方法,目的是即使你有了这个log库,但我仍旧希望你可以能找到更好的方法来达到调试的目的。拥有技巧,使用技巧,最终化为无形才是最高境界。相信我们的最终目的是一致的,那就是让开发越来越简便,越来越优雅~

    最后说下我没直接用文章开头那几个库的原因,logger的库很漂亮,但是冗余行数过多,调试多行的数据就会受到信息干扰。timber的本身设计就是一个log的框架,打印是交给开发者自定义的,所以我将timber的框架和logger的美观实现进行了结合。这当然还要感谢logUtils的作者,让log支持了object类型。

    有朋友问,你为什么不自己实现log框架,而是依赖于timber做呢,这样会不会太重?其实logger的1.1.6版本中,我确实是自己实现了所有的功能,没有依赖于任何库。当我看到了timber后,我发现我做的工作和这个库的重叠性太高了,而且它的设计也很值得学习。于是我直接依赖于它做了重构,我现在只关心log的美化和功能的扩展,log分发的事情就交给timber了。

    developer-kale@foxmail.com 微博:@天之界线2010

    参考文章:

    相关文章

      网友评论

      • 天煞魔猎手:关于使用assumenosideeffects的混淆配置时,实际上只是仅仅把Log类相关方法去掉,但是如果打log方法中使用了表达式之类的话,其实是不会去掉的,比如:

        XXXLog.i("tag", "%s", aaa.toString());
        XXXLog.e("tag", new Exception("自定义Exception输出"));

        第一句代码,混淆之后会变为 aaa.toString();
        第二句代码混淆之后,会变为 new Exception(“自定义Exception输出”)

        即Log方法中的内容实际上并不会去掉,而且混淆开启时,这些表达式也不会去掉,那么就以上面两例子来说:

        第一句:我Log都去掉了,但是还执行aaa.toString()方法,实际上是没必要的
        第二句:同样,Log都去掉,是没必要new 一个Exception的,但是实际上还是会执行

        所以个人据地assumnosideeffects 的效果其实很一般
        天之界线2010:@蔡枝涛 果真是认证的人。
        天煞魔猎手:感谢回复,我这边之所以执着于这个问题是因为我希望想实现Log在开发时输出,打包时则移除所有的Log代码(而不是仅仅不输出log,但实际上还是会执行Log参数中的表示式),一般而言可以使用BuildConfig来进行控制,比如一般大概都是按照文章中的章节《增加自动化或强制开关》来进行实现,如在每次打Log之前加上一个判断,if(BuildConfig.DEBUG) { Log.i(...)}, 那么在设置BuildConfig.DEBUG设置为false之后,开启混淆后确实会整句代码移除掉。但是这里存在一个问题就在于如果代码中打Log的地方比较多,会发现每次都这样子写的话,代码会变得有点长,非常不方便其他同学阅读,于是,我这边就开始考虑有没有什么办法,在开发时,不用写if(BuildConfig.DEBUG),只需要写 Log.i(...) 也可以实现发布时移除所有Log的方案,结果尝试了一下assumenosideeffects这个混淆配置,发现不太理想,后来也尝试使用TransformAPI + Javaassist来尝试在编译时自动移除整句代码,但是发现java的字节码修改并太理想,实际移除后的结果和使用assumenosideeffects差不多,目前暂时也没有什么思路,倒是在搜索其他方案时拜读到这篇文章,好文
        天之界线2010:@蔡枝涛 感谢测试,我也感觉收益比很低
      • initialjie:总体不错,但是有个问题,外部想自定义printer时,还是依赖了Timber.Tree这个包。
        orhanobut/logger是自己提供了一个adapter接口,但他也有其他的不好的地方,就是自己格式化Log样式,这样又不灵活了。
        天之界线2010:@initialjie 完全同意,这也是这个库的目的。希望可以通过qq交流一下,460412008
        initialjie:@initialjie 看了代码,总体就是一个格式化输出的Timber封装
      • snowdream:思路值得借鉴
      • 小爨:真的很好
        天之界线2010:@小爨 460412008@qq.com 加我
        小爨:@天之界线2010 我把Logger导入到项目中,无法打印log啊,我也按照你文档要求的步骤做了,唯一的区别是我的buildtools 是23.0.3 你的是24
        天之界线2010:@小爨 感谢
      • fe2a5bcdf6ea:很棒
      • sing_song:写的很好,学习了。
      • wjehovah:这才是最佳实践。
        天之界线2010:@wjehovah 感谢!

      本文标题:Log最佳实践

      本文链接:https://www.haomeiwen.com/subject/lmjhjttx.html