Skip to content

WhyUsePLog

Muyangmin edited this page Feb 23, 2017 · 2 revisions

我为什么要写PLog

在编程世界里存在很多这样的例子:标准库往往是万能的,可以解决几乎所有问题~~(如果不能,那就升级标准库)~~, 但它往往也使用起来不很方便,需要二次封装来使用。

就Android开发的日志打印需求来说,标准库提供的android.util.Log类,使用起来就不够方便。

标准日志库的日常

乱入进程的log

即使在Logcat中选择了只看当前进程,也会有各种奇怪的日志分散注意力,比如下面这种:

对,我想说的就是你,那个做ROM的同学

注意,这里的截图仅仅是为了举例说明问题,并不是针对某个ROM。实际上大部分ROM都有各种各样的日志没在release的时候去掉。

总也打不完的Tag

标准日志类Log提供了v/d/i/w/e(String, String, [Throwable])系列方法,这些方法有一个共同点:必须提供一个tag。这个tag在做筛选的时候很有用,但是每次打日志都需要多传一个参数又很麻烦。 早期Android开发中,我习惯在BaseActivity里定义一个LOG_TAG字段, 就像这样:

public class BaseActivity extends Activity {
    protected final String LOG_TAG = getClass().getSimpleName();
}

后来,在Fragment里也常常需要打日志,于是又在BaseFragment里再加一个一模一样的代码。
问题来了,我还可能在工具类、自定义View类和网络接口回调里打日志呢?总不能再搞个基类吧?

长度和格式限制

如果我们希望打印网络返回日志,而又没有做特殊处理的话,那么我们打印出来的日志很有可能被截断,因为Logcat一行最长只有4000个字符,超出的字符会被截断。当你看着成百上千字符的JSON串挤在一行的时候,你的眼睛还好吗?

现在的第三方日志库

上述问题自然有各种各样的解决方案。其中有最简单的只加了一个isDebug 开关的类,也有复杂的日志库。包装类的方式最普遍,但是往往只能解决输出的控制问题,不能解决便利性问题。这里主要讲讲我见过的第三方日志库。

先定义我对日志库比较的几个维度:

  1. 容易打印:这是最基本、最重要的要求,希望一行代码就能打印任何类型的数据,必要的时候可以指定一些额外参数,但不能写额外的一堆代码。
  2. 筛选容易这也是一个非常重要的特性,当日志量大的时候非常有用。一般要求能够自动打印当前类名,另外筛选的维度尽可能要多样化,以应付日志过多的场景。
  3. 支持定位:Android Studio的控制台会对包含特定格式字符串的文本追加定位功能,在看日志的时候小手一点就可以看到打日志的地方,省时省力。
  4. 日志美观:可以支持换行和美化效果,但要有一定的度。个人不喜欢过度复杂的花哨日志,相比看一堆线框,我宁愿用标准库。
  5. 输出可控:不仅要求能多向输出(比如SD卡和第三方Crash监测平台),还要能在需要的时候做拦截和关闭输出的操作。
  6. 设置灵活:每个APP对日志的需求都不相同,所有非核心信息都应该可以由调用者控制是否显示,比如线程信息、堆栈信息和格式化信息。
  7. 体积轻巧:日志只是一个辅助开发工具,如果为了使用这个功能要引入额外的大体积依赖包,有点得不偿失。
  8. 上手简单:学习成本不能过高。

OK,标准定义好了,来看看比较结果(部分选项可能存在主观影响,仅代表个人看法)。

Library Name Logger Timber KLog
Star/Fork 5.7K+/1.0K+ 3.5K+/366 1.1K+/251
容易打印
支持定位 ×
筛选容易* × ☆☆
日志美观* ×
输出可控* × ☆☆
设置灵活
体积轻巧 ☆☆
上手简单

所有数据截止日期为 2017-02-16, 17:00.

解释一下:

  • 筛选容易:KLog可以支持用类名做Tag,Logger将类名信息放到了消息体中,而Timber并没有这一功能。
  • 日志美观:LoggerKLog绘制了行分割线等装饰,Logger更是可以将一行消息打印为8行,过于花哨,反而影响查看日志本身;而Timber则并没有做这方面的处理,也就没有解决Logcat 4K截断问题。
  • 输出可控:Logger不支持重定向输出;Timber可以支持同时向多个通道输出日志,但没有统一的拦截方式;**KLog **虽然可以输出到文件,但无法同时多通道打印,也没有统一的拦截方式。

按惯例,当现成的轮子不符合实际需求的时候,就该造新的轮子了。

PLog——让打日志更轻松!

自动Tag和空消息打印

上回书说到打不完的Tag会很累人,那么最基本的自动Tag功能自然是少不掉的,不过这个是作为一个设置项,需要手动开启。

PLog.init(PLogConfig.newBuilder().useAutoTag(true).build());

自动Tag是指将打印日志的位置的类名作为Tag来打印日志。

如果你只是希望观察某个地方是否被执行过,那么你可能会喜欢空消息打印的操作:

PLog.empty();

当然,如果你喜欢使用Log.INFO以上的日志等级,你也可以在设置时对所有的空消息日志等级做一次调整。

全局Tag和分组打印

在Android Monitor里可以看到,Logcat工具中能让我们做筛选功能的维度有:

  • Log Tag
  • Log Message
  • Package Name
  • PID
  • Log Level

其中,

  • Package Name和PID对于日志库的意义不大;
  • Log level 应当是开发者在具体调用的时候选择,而且取值有限;
  • Log message 应该尽可能保持调用时的参数原貌,不能过分侵入修改。

因此在经过思考后,我为PLog引入了Global TagCategory 的概念,其本质仍然是利用tag来扩展日志的筛选维度。其中,Global Tag是静态的(相对于应用本身),用于区分开发者自己的日志与Android Framework等其他进程内日志。而Category则是动态的,可以将应用内不同开发者的日志做分组操作,便于筛选。 使用方法:

//Init config using global tag
PLog.init(PLogConfig.newBuilder().globalTag("MyApp")
        .forceConcatGlobalTag(true).build());

PLog.d("test");     //sample output:   D/MyApp: test

PLog.tag("explicitTag").d(test);
                    //sample output:   D/MyApp-explicitTag: test

PLog.tag("MainActivity").category("Alex").d("test").execute();
                    //sample output:   D/MyApp-MainActivity[Category:Alex]: test

Category 仅在 PLog 2.x 版本上可用。

变长参数和格式化

打日志时往往需要观察各种类型的参数的值,这些类型可能是int、boolean,也可能是ArrayList、HashMap,甚至可以是JSONObject或其他POJO 类对象。而且不同参数之间的拼接也很成问题,不知道你是否写过这样的代码:

    Log.d("tag", "compute finished, x=" + x + ", y=" + y + ", distance=" + distance);

很熟悉吧?有了PLog,就不需要再去写这些麻烦的引号和加号了:

   PLog.d("x=%d, y=%d, distance=%.2f", x, y, distance);
   //Or:
   PLog.objects(x, y, distance);

有没有很方便?

  • 上面的例子里已经展示了在v/d/i/w/e(msg, obj...)方法中传递格式化参数的方案,因为底层实现是基于String.format() ,所以这里可以接受符合Java格式化规则的任何符号。
  • 基于保持模块相对独立和依赖包体积的考虑,如果你需要格式化操作 POJO 和 JSON 对象,你需要引入plog-formatter依赖包,此外无需做任何操作。

美化日志

虽然我不喜欢把日志输出绘制得很复杂,但是如果你需要,也可以在不改动PLog源码的情况下实现线框等图案。

只要自己定义一个Style接口并应用到相应的Printer即可:

Style borderStyle = new Style() {

        String LINE = "------------------------------------------------";

        /**
         * Prefix of each log.
         * @return can be null
         */
        @Nullable
        String msgPrefix() {
            return LINE + "\n";
        }

        /**
         * Suffix of each log.
         * @return can be null
         */
        @Nullable
        String msgSuffix() {
            return "\n" + LINE;
        }

        /**
         * the line header property is only effected when soft wrap is enabled.
         * @return the string of each line header, e.g. |.
         */
        @Nullable
        String lineHeader() {
            return "|";
        }
};

printer.setStyle(borderStyle);

拦截和控制输出

我们先分析一下几个相关需求:

假如需要关闭所有日志

这很简单,只需要在封装日志类里加一个取值为BuildConfig.DEBUG的开关即可。

假如需要关闭所有INFO级别以下的日志

这也还好,需要使用两个开关,对于INFO级别以下的日志要使用逻辑与做开关。

假如需要对某个特定场景下的日志(比如关闭所有网络日志)做开关

这个就比较麻烦了,很可能需要在设置项里做一个网络日志开关,并且在网络层每个打日志的地方都要判断开关的取值~~(某些APP确实是这么做的)~~。你也可以为网络层单独再次封装一个全功能日志类。

有点头疼了吧?

假如需要同时打印日志到控制台和文件,同时在文件中不出现INFO级别以下的日志……

PLog中引入了拦截器(interceptor)的概念,分为全局拦截器和通道拦截器,可以很容易地解决上述问题。

    /**
     * Indicate whether this item of log should be intercepted.
     *
     * @param level    print level of this log.
     * @param category category of this log, if specified.
     * @param msg      content of this log(before formatting!).
     * @return if returns true, this log won't be printed and just be ignored. Otherwise it would
     * be formatted and printed as usual.
     */
    @CheckResult
    boolean onIntercept(@PrintLevel int level, @NonNull String tag,
                        @Nullable Category category, @NonNull String msg) {
        //过滤所有日志
        return true;

        //过滤INFO级别以下日志
        return level <= Log.INFO;

        //过滤网络层日志(网络层日志使用了名为"network"的category)
        return category.getName().equalsIgnorecase("network");

        //在文件通道中过滤:只需要对相应的printer进行设置即可,其写法一样,此处从略。
    }

Lint 检查

PLog将在2.x正式版本中实现Lint功能,拟包括以下检查项:

  • 使用原生Log类(推荐使用PLog或包装类)
  • 使用了返回类型为LogRequest的方法但并没有调用execute()

协助改进PLog

PLog立志做最好用的日志库,2.0版本自身也是在1.x版本的实践基础上演变而来的。如果在使用上遇到任何问题,欢迎使用GitHub Issue功能交流沟通。也非常欢迎大家一起完善这个库。

GitHub项目地址: https://github.com/Muyangmin/Android-PLog