XLog 详解及源码分析

作者: 仰简 | 来源:发表于2018-11-20 00:55 被阅读26次

    一、前言

    这里的 XLog 不是微信 Mars 里面的 xLog,而是elvishewxLog。感兴趣的同学可以看看作者 elvishwe 的官文史上最强的 Android 日志库 XLog。这里先过一下它的特点以及与其他日志库的比较。文章主要分析 xLog 中的所有特性的实现,以及作为一个日志工具,它实际的需求是什么。
    特点

    1.全局配置(TAG,各种格式化器...)或基于单条日志的配置
    2.支持打印任意对象以及可自定义的对象格式化器
    3.支持打印数组
    4.支持打印无限长的日志(没有 4K 字符的限制)
    5.XML 和 JSON 格式化输出
    6.线程信息(线程名等,可自定义)
    7.调用栈信息(可配置的调用栈深度,调用栈信息包括类名、方法名文件名和行号)
    8.支持日志拦截器
    9.保存日志文件(文件名和自动备份策略可灵活配置)
    10.在 Android Studio 中的日志样式美观
    11.简单易用,扩展性高

    与其他日志库的区别

    1.优美的源代码,良好的文档
    2.扩展性高,可轻松扩展和强化功能
    3.轻量级,零依赖

    二、源码分析

    1.官文架构

    image.png

    2.全局配置及其子组件介绍

    // 日志输出样式配置
    LogConfiguration config = new LogConfiguration.Builder()
        .tag("MY_TAG")                                         // 指定 TAG,默认为 "X-LOG"
        .t()                                                   // 允许打印线程信息,默认禁止
        .st(2)                                                 // 允许打印深度为2的调用栈信息,默认禁止
        .b()                                                   // 允许打印日志边框,默认禁止
        .jsonFormatter(new MyJsonFormatter())                  // 指定 JSON 格式化器,默认为 DefaultJsonFormatter
        .xmlFormatter(new MyXmlFormatter())                    // 指定 XML 格式化器,默认为 DefaultXmlFormatter
        .throwableFormatter(new MyThrowableFormatter())        // 指定可抛出异常格式化器,默认为 DefaultThrowableFormatter
        .threadFormatter(new MyThreadFormatter())              // 指定线程信息格式化器,默认为 DefaultThreadFormatter
        .stackTraceFormatter(new MyStackTraceFormatter())      // 指定调用栈信息格式化器,默认为 DefaultStackTraceFormatter
        .borderFormatter(new MyBoardFormatter())               // 指定边框格式化器,默认为 DefaultBorderFormatter
        .addObjectFormatter(AnyClass.class,                    // 为指定类添加格式化器
                new AnyClassObjectFormatter())                 // 默认使用 Object.toString()
        .build();
    
    // 打印器
    Printer androidPrinter = new AndroidPrinter();             // 通过 android.util.Log 打印日志的打印器
    Printer SystemPrinter = new SystemPrinter();               // 通过 System.out.println 打印日志的打印器
    Printer filePrinter = new FilePrinter                      // 打印日志到文件的打印器
        .Builder("/sdcard/xlog/")                              // 指定保存日志文件的路径
        .fileNameGenerator(new DateFileNameGenerator())        // 指定日志文件名生成器,默认为 ChangelessFileNameGenerator("log")
        .backupStrategy(new MyBackupStrategy())                // 指定日志文件备份策略,默认为 FileSizeBackupStrategy(1024 * 1024)
        .logFlattener(new MyLogFlattener())                    // 指定日志平铺器,默认为 DefaultLogFlattener
        .build();
    

    全局配置主要是为了根据业务需求进行相关的配置。xLog 的配置可以分成 2 个大类别:日志的输出样式以及日志输出的打印器配置。

    LogConfiguration
    LogConfiguration 的构造用是 Builder 设计模式。对于属性配置类,一般由于会有比较多的配置项,并且一般都会设定其默认配置值,所以大多都会选择采用 Builder 设计模式。

    LogConfiguration.jpg
    上图是一个在 Builder 设计模式下的严格定义,但一般情况下,如果只需要 builder 出一个 “产品”,那么完全不需要再抽象出一个 builder 接口,而是直接使用具体类型的 builder 即可。否则就会出现过度设计的问题。

    Formatter
    Formatter 主要是为一些常见的对象提供格式化的输出。XLog 中抽你了一个泛型接口 Formatter,其中的 format() 方法定义了输入一个数据/对象,对应将其格式化成一个 String 用于输出,中间的处理过程由各个子类自己完成。

    /**
     * A formatter is used for format the data that is not a string, or that is a string but not well
     * formatted, we should format the data to a well formatted string so printers can print them.
     *
     * @param <T> the type of the data
     */
    public interface Formatter<T> {
    
      /**
       * Format the data to a readable and loggable string.
       *
       * @param data the data to format
       * @return the formatted string data
       */
      String format(T data);
    }
    

    如下是框架内定义的各类 Formatter:Object,Json,Border,Throwable,Xml,StackTrace,Thread 共 7 个接口,每个接口下又都提供了默认的具类 DefaultXXXFormatter。我们可以通过实现这 7 个接口,来定义自己的具类 Formatter,从而定义自己的输出格式,并通过LogConfiguration 相应的 xxxFormatter() 方法来控制 formatter。


    Formatter.jpg

    Printer
    Printer 的主要功能是控制日志的输出渠道,可以是 Android 的日志系统,控制台,也可以是文件。XLog 中抽象出了 Printer 接口,接口中的 println() 方法控制实际的输出渠道。

    **
     * A printer is used for printing the log to somewhere, like android shell, terminal
     * or file system.
     * <p>
     * There are 4 main implementation of Printer.
     * <br>{@link AndroidPrinter}, print log to android shell terminal.
     * <br>{@link ConsolePrinter}, print log to console via System.out.
     * <br>{@link FilePrinter}, print log to file system.
     * <br>{@link RemotePrinter}, print log to remote server, this is empty implementation yet.
     */
    public interface Printer {
    
      /**
       * Print log in new line.
       *
       * @param logLevel the level of log
       * @param tag      the tag of log
       * @param msg      the msg of log
       */
      void println(int logLevel, String tag, String msg);
    }
    

    如下是框架定义的各类 Printer,一共 5 个。其中 AndroidPrinter,FilePrinter,ConsolePrinter,RemotePrinter 可以看成单一可实际输出的渠道。而 PrinterSet 是包含了这些 Printer 的组合,其内部实现就是通过循环迭代每一个 printer 的 println() 方法,从而实现同时向多个渠道打印日志的功能。

    Printer.jpg
    AndroidPrinter 调用了 Android 的日志系统 Log,并且通过分解 Log 的长度,按最大 4K 字节进行划分,从而突破 Android 日志系统 Log 对于日志 4K 的限制。
    FilePrinter 通过输出流将日志写入到文件,用户需要指定文件的保存路径、文件名的产生方式、备份策略以及清除策略。当然,对于文件的写入,是通过在子线程中进行的。如下分别是清除策略以及备份策略的定义。清除策略是当日志的存放超过一定时长后进行清除或者不清除。备份策略是当日志文件达到一定大小后便将其备份,并产生一个新的文件以继续写入。
    CleanStrategy.jpg BackupStrategy.jpg

    ConsolePrinter 通过 System.out 进行日志的输出
    RemotePrinter 将日志写入到远程服务器。框架内的实现是空的,所以这个其实是需要我们自己去实现。
    除了以上 4 个框架内定义好的 printer,用户还可以通过实现 Printer 接口实现自己的 printer。

    Flatter
    Flatter 的主要作用是在 FilePrinter 中将日志的各个部分(如time,日志 level,TAG,消息体)按一定规则的衔接起来,组成一个新的字符串。需要注意的是框架现在提供的是 Flattener2,而原来的 Flattener 已经被标记为过时。

    /**
     * The flattener used to flatten log elements(log time milliseconds, level, tag and message) to
     * a single CharSequence.
     *
     * @since 1.6.0
     */
    public interface Flattener2 {
    
      /**
       * Flatten the log.
       *
       * @param timeMillis the time milliseconds of log
       * @param logLevel  the level of log
       * @param tag       the tag of log
       * @param message   the message of log
       * @return the formatted final log Charsequence
       */
      CharSequence flatten(long timeMillis, int logLevel, String tag, String message);
    }
    

    框架里为我们定义了 2 个默认的 Flatter,DefaultFlattener 和 PatternFlattener,其类图如下。


    Flattener2.jpg

    DefaultFlattener 默认的 Flattener 只是简单的将各部分进行拼接,中间用 “|” 连接。

    @Override
      public CharSequence flatten(long timeMillis, int logLevel, String tag, String message) {
        return Long.toString(timeMillis)
            + '|' + LogLevel.getShortLevelName(logLevel)
            + '|' + tag
            + '|' + message;
      }
    

    PatternFlattener 要稍微复杂一些,其使用正则表达式规则对各部分进行适配再提取内容,其支持的参数如下。

    序号 Parameter Represents
    1 {d} 默认的日期格式 "yyyy-MM-dd HH:mm:ss.SSS"
    2 {d format} 指定的日期格式
    3 {l} 日志 level 的缩写. e.g: V/D/I
    4 {L} 日志 level 的全名. e.g: VERBOSE/DEBUG/INFO
    5 {t} 日志TAG
    6 {m} 日志消息体

    我们将需要支持的参数拼接到一个字串当中,然后由 PatternFlattener 将其进行分解并构造出对应的 **Filter,在其 flatten() 方法中,会通过遍历的方式询问每个 filter 是否需要进行相应的替换。

    @Override
      public CharSequence flatten(long timeMillis, int logLevel, String tag, String message) {
        String flattenedLog = pattern;
        for (ParameterFiller parameterFiller : parameterFillers) {
          flattenedLog = parameterFiller.fill(flattenedLog, timeMillis, logLevel, tag, message);
        }
        return flattenedLog;
      }
    

    当然,除此之外,我们还可以定义自己的 Flatter,如作者所说,可以将其用于对 Log 的各个部分有选择的进行加密等功能。

    Interceptor
    interceptor 与 OkHttp 中 interceptor 有点类似,也同样采用了职责链的设计模式,其简要的类图如下。

    Interceptor.jpg
    可以通过在构造 LogConfiguration 的时候,通过其 Builder 的 addInterceptor() 方法来添加 interceptor。对于每个日志都会通过遍历 Interceptor 进行处理,处理的顺序按照添加的先后顺序进行。而当某个 interceptor 的 intercept() 方法返回 null 则终止后面所有的 interceptor 处理,并且这条日志也将不会再输出。

    以上便是对 XLog 框架中所定义的子组件的简要分析,共包括:LogConfiguration,Formatter,Printer,Flatter,Interceptor。通过对整体框架的认识以及各个子组件的分析,从而使得我们可以熟知整个框架的基本功能。

    3.初始化

    XLog#init()
    经过全局配置后,便会调用 XLog#init() 方法进行初始化。

    //初始化
    XLog.init(LogLevel.ALL,                                    // 指定日志级别,低于该级别的日志将不会被打印
        config,                                                // 指定日志配置,如果不指定,会默认使用 new LogConfiguration.Builder().build()
        androidPrinter,                                        // 添加任意多的打印器。如果没有添加任何打印器,会默认使用 AndroidPrinter
        systemPrinter,
        filePrinter);
    

    init() 方法有多个重载的,我们仅看相关的即可。

    /**
       * Initialize log system, should be called only once.
       *
       * @param logConfiguration the log configuration
       * @param printers         the printers, each log would be printed by all of the printers
       * @since 1.3.0
       */
      public static void init(LogConfiguration logConfiguration, Printer... printers) {
        if (sIsInitialized) {
          Platform.get().warn("XLog is already initialized, do not initialize again");
        }
        sIsInitialized = true;
    
        if (logConfiguration == null) {
          throw new IllegalArgumentException("Please specify a LogConfiguration");
        }
        // 记录下全局配置
        sLogConfiguration = logConfiguration;
        // 将所有的 printer 汇合成一个 PrinterSet 集合
        sPrinter = new PrinterSet(printers);
        // 初始化 Logger
        sLogger = new Logger(sLogConfiguration, sPrinter);
      }
    

    从上面的代码来看,其主要就是记录下了状态,及其 3 个静态变量 sLogConfiguration,sPrinter以及 sLogger,而 sLogConfiguration和sPrinter又拿来初始化了 sLogger,其关系如下类图所示。


    XLog.jpg

    Logger 类是日志中的核心类,其真正持有了 LogConfiguration 和 PrinterSet,并通过调度 LogConfiguration 和 PrinterSet 来进行日志的输出。

    4.日志的输出

    XLog#d(String, Throwable)
    这里以 XLog.d(String, Throwable) 这个方法来分析一下日志的打印,其他的过程上是类似的

    /**
       * Log a message and a throwable with level {@link LogLevel#DEBUG}.
       *
       * @param msg the message to log
       * @param tr  the throwable to be log
       */
      public static void d(String msg, Throwable tr) {
        assertInitialization();
        sLogger.d(msg, tr);
      }
    

    再进一步看 Logger#d()

    /**
       * Log a message and a throwable with level {@link LogLevel#DEBUG}.
       *
       * @param msg the message to log
       * @param tr  the throwable to be log
       */
      public void d(String msg, Throwable tr) {
        println(LogLevel.DEBUG, msg, tr);
      }
    
    /**
       * Print a log in a new line.
       *
       * @param logLevel the log level of the printing log
       * @param msg      the message you would like to log
       * @param tr       a throwable object to log
       */
      private void println(int logLevel, String msg, Throwable tr) {
       // 控制 debug level
        if (logLevel < logConfiguration.logLevel) {
          return;
        }
        // 将 Throwable 进行格式化,然后调用 printlnInternal()方法进行日志的输出。
        printlnInternal(logLevel, ((msg == null || msg.length() == 0)
            ? "" : (msg + SystemCompat.lineSeparator))
            + logConfiguration.throwableFormatter.format(tr));
      }
    

    上面代码最终就是走到了 printlnInternal() 方法,这是一个私有方法,而不管前面是调用哪一个方法进行日志的输出,最终都要走到这个方法里面来。

    /**
       * Print a log in a new line internally.
       *
       * @param logLevel the log level of the printing log
       * @param msg      the message you would like to log
       */
      private void printlnInternal(int logLevel, String msg) {
        // 获取 TAG
        String tag = logConfiguration.tag;
        // 获取线程名称
        String thread = logConfiguration.withThread
            ? logConfiguration.threadFormatter.format(Thread.currentThread())
            : null;
        // 获取 stack trace,通过 new 一个 Throwable() 就可以拿到当前的 stack trace了。然后再通过设置的 stackTraceOrigin 和 stackTraceDepth 进行日志的切割。
        String stackTrace = logConfiguration.withStackTrace
            ? logConfiguration.stackTraceFormatter.format(
            StackTraceUtil.getCroppedRealStackTrack(new Throwable().getStackTrace(),
                logConfiguration.stackTraceOrigin,
                logConfiguration.stackTraceDepth))
            : null;
        // 遍历 interceptor,如果其中有一个 interceptor 返回了 null ,则丢弃这条日志
        if (logConfiguration.interceptors != null) {
          LogItem log = new LogItem(logLevel, tag, thread, stackTrace, msg);
          for (Interceptor interceptor : logConfiguration.interceptors) {
            log = interceptor.intercept(log);
            if (log == null) {
              // Log is eaten, don't print this log.
              return;
            }
    
            // Check if the log still healthy.
            if (log.tag == null || log.msg == null) {
              throw new IllegalStateException("Interceptor " + interceptor
                  + " should not remove the tag or message of a log,"
                  + " if you don't want to print this log,"
                  + " just return a null when intercept.");
            }
          }
    
          // Use fields after interception.
          logLevel = log.level;
          tag = log.tag;
          thread = log.threadInfo;
          stackTrace = log.stackTraceInfo;
          msg = log.msg;
        }
        // 通过  PrinterSet 进行日志的输出,在这里同时也处理了日志是否需要格式化成边框形式。
        printer.println(logLevel, tag, logConfiguration.withBorder
            ? logConfiguration.borderFormatter.format(new String[]{thread, stackTrace, msg})
            : ((thread != null ? (thread + SystemCompat.lineSeparator) : "")
            + (stackTrace != null ? (stackTrace + SystemCompat.lineSeparator) : "")
            + msg));
      }
    

    代码相对比较简单,主要的步骤也都写在注释里面,就不再一一描述了。至此,XLog 的主要框架也基本分析完了。同时,也感谢作者无私的开源精神,向我们分享了一个如此简单但很优秀的框架。

    三、后记

    感谢你能读到并读完此文章。希望我的分享能够帮助到你,如果分析的过程中存在错误或者疑问都欢迎留言讨论。

    相关文章

      网友评论

        本文标题:XLog 详解及源码分析

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