一、前言
这里的 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.官文架构
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 设计模式。
上图是一个在 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 the type of the data
*/
public interface Formatter {
/**
* 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。
Printer
Printer 的主要功能是控制日志的输出渠道,可以是 Android 的日志系统,控制台,也可以是文件。XLog 中抽象出了 Printer 接口,接口中的 println() 方法控制实际的输出渠道。
**
* A printer is used for printing the log to somewhere, like android shell, terminal
* or file system.
*
* There are 4 main implementation of Printer.
*
{@link AndroidPrinter}, print log to android shell terminal.
*
{@link ConsolePrinter}, print log to console via System.out.
*
{@link FilePrinter}, print log to file system.
*
{@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() 方法,从而实现同时向多个渠道打印日志的功能。
AndroidPrinter 调用了 Android 的日志系统 Log,并且通过分解 Log 的长度,按最大 4K 字节进行划分,从而突破 Android 日志系统 Log 对于日志 4K 的限制。
FilePrinter 通过输出流将日志写入到文件,用户需要指定文件的保存路径、文件名的产生方式、备份策略以及清除策略。当然,对于文件的写入,是通过在子线程中进行的。如下分别是清除策略以及备份策略的定义。清除策略是当日志的存放超过一定时长后进行清除或者不清除。备份策略是当日志文件达到一定大小后便将其备份,并产生一个新的文件以继续写入。
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,其类图如下。
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 有点类似,也同样采用了职责链的设计模式,其简要的类图如下。
可以通过在构造 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,其关系如下类图所示。
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 的主要框架也基本分析完了。同时,也感谢作者无私的开源精神,向我们分享了一个如此简单但很优秀的框架。
三、后记
感谢你能读到并读完此文章。希望我的分享能够帮助到你,如果分析的过程中存在错误或者疑问都欢迎留言讨论。