Log最佳实践

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

本文固定链接:github.com/tianzhijiex…php


1、背景

Android中的log是这么写的:html

Log.d(TAG, "This is a debug log");复制代码

android.util.Log类作的事情很简单,符合kiss原则,可是随着业务的不断发展,logcat中就会有多个部门的各类log,不一样手机系统本身的一些log也会参杂进来,逼迫咱们要扩展log类。java

2、需求

  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

回看这些需求,不合理么?其实很合理,咱们的宗旨就是让无心义的重复代码去死,若是死不掉就交给机器来作。咱们应该作那些真正须要咱们作的事情,而不是像一个没思想的猿猴通常成天写模板式代码。这才是程序员思惟,而不是程序猿思惟!android

注意:我但愿只要写真正有意义的内容!git

3、实现

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

使用篇

创建包装类

不管一个第三方库有多好,我仍是推荐不直接使用它,由于你颇有可能会去替换这个第三方库,并且一个第三方库确定没法知足各类奇葩需求。因此,对于网络库、图片库和log库来讲,咱们应该事先考虑在上面封装一层。github

咱们创建一个包装类,用这个包装类用来包裹Logger(logger是本文介绍的一个log库),下面是包装类的代码片断:json

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的模板提示的功能:

咱们能够模仿原有的模板来作本身的代码模板,简化模板式代码的输入。至于具体模仿的方式我就不手把手教了,至关简单。下面仅展现下自带的log模板的使用:

生成TAG:


自动填写参数和方法名:

显示篇

让log更加美观

让log的输出直观、美观其实很简单,就是在输出前作点字符串拼接的工做,好比加上下面这行横线。

private static final String BOTTOM_BORDER = "╚═══════════════════════════";复制代码

由于作了不少拼接的工做,因此好看的log也是消耗性能的。个人习惯是调试完毕后马上删除无用的log,这样既能减小性能影响,也能减小同事的阅读代码的负担。采用轻量级美化后效果以下:

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

这个功能其实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),这样既能防护问题,又能够帮助开发者知道崩溃产生的缘由,方便之后针对性的进行处理。

代码参考自:blog.xmartlabs.com/2015/07/09/…

模拟

/** * 这里模拟后端给客户端传值的状况。 * * 这里的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. 经过console热部署打印log信息
    我经过debug工具,能够在任意位置打印出任意对象的值,经过这种方式就能够精准调试一些信息了。
    下图是我让其在不中断运行的状况下打印index的值。

  2. 动态设置值
    有时候某种分支须要在某个状况下才能走到,我能够利用debug的setValue(F12)方法动态设置值,好比我把下面的123改为了520,最终在终端打印出的信息也会变成520。整个过程对本来代码彻底屏蔽,无入侵。

PS:更多的调试技巧能够查看Android-Best-Practices中的推荐的调试技巧的文章。

因地制宜的使用log

虽然我提出了上面的思路和方案,但我并不能确保能够知足全部的需求,我给出下面的思惟流程,方便你们随机应变:

  1. 尽可能用as的debug模式下的log系统,无入侵。不用写代码就能打log,十分方便。
  2. 若是真的要打log作调试,先用debug和error级别,提交代码时务必记得清除。
  3. 若是提交的代码中须要在某个关键点打log,或者要持续调试,能够用info以上的log。
  4. 在realse中用本身的log包装类的开关作处理,这样方便在公司内部测试时能够查看到log。
  5. 若是一些信息须要在用户版本中保留,优先考虑数据统计的方式进行关键点的打点。
  6. 若是真的要在发布出去的apk中带着log,只保留info级别以上的,不轻易把info级别之下的信息漏出去。

4、总结

咱们能够看到即便一行代码的log都有不少点是可优化的,还明白了咱们以前一直写的模板式代码是多么的枯燥乏味。
经过这篇文章,但愿你们能够看到一个优化编码的思惟过程,也但愿你们去尝试下logger这个库。固然,我知道仍是有不少人不喜欢,那么不妨提出更好的解决方案来一块儿讨论,不满意能够提issue。
要知道精品永远是个位数,而中庸的东西永远是层出不穷的。我但愿你们多提意见齐心合力优化出一个精品,而不是花时间去在平庸的选项中作着选择难题。

5、尾声

在文章中我给出了经过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

参考文章:

相关文章
相关标签/搜索