XLog 详解及源码分析
来源:仰简     阅读:2252
微上宝科技
发布于 2018-11-20 23:18
查看主页

一、前言

这里的 XLog 不是微信 Mars 里面的 xLog,而是elvishew的xLog。感兴趣的同学可以看看作者 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 要略微复杂少量,其使用正则表达式规则对各部分进行适配再提取内容,其支持的参数如下。

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

三、后记

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

免责声明:本文为用户发表,不代表网站立场,仅供参考,不构成引导等用途。 系统环境 服务器应用
相关推荐
深入了解feign(02)-源码解析
C# 实现连接SQL Server
export default与export,module.exports与exports的区别和关系
Hadoop 之父:普通程序员到顶级公司 CTO 的进阶之路
wepy项目的学习
首页
搜索
订单
购物车
我的