
这里的 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.轻量级,零依赖
image.png// 日志输出样式配置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.jpgFormatter
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.jpgPrinter
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
CleanStrategy.jpg
BackupStrategy.jpgConsolePrinter 通过 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.jpgDefaultFlattener 默认的 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以上便是对 XLog 框架中所定义的子组件的简要分析,共包括:LogConfiguration,Formatter,Printer,Flatter,Interceptor。通过对整体框架的认识以及各个子组件的分析,从而使得我们可以熟知整个框架的基本功能。
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.jpgLogger 类是日志中的核心类,其真正持有了 LogConfiguration 和 PrinterSet,并通过调度 LogConfiguration 和 PrinterSet 来进行日志的输出。
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 的主要框架也基本分析完了。同时,也感谢作者无私的开源精神,向我们分享了一个如此简单但很优秀的框架。
感谢你能读到并读完此文章。希望我的分享能够帮助到你,假如分析的过程中存在错误或者者疑问都欢迎留言探讨。