XLog 详解及源码分析

1,428 阅读11分钟
原文链接: www.jianshu.com

一、前言

这里的 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 的主要框架也基本分析完了。同时,也感谢作者无私的开源精神,向我们分享了一个如此简单但很优秀的框架。

三、后记

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