XLog 詳解及原始碼分析
一、前言
這裡的 XLog 不是微信 Mars 裡面的 xLog,而是 ofollow,noindex">elvishew 的 xLog 。感興趣的同學可以看看作者 elvishwe 的官文 史上最強的 Android 日誌庫 XLog 。這裡先過一下它的特點以及與其他日誌庫的比較。文章主要分析 xLog 中的所有特性的實現,以及作為一個日誌工具,它實際的需求是什麼。
特點
1.全域性配置(TAG,各種格式化器...)或基於單條日誌的配置
2.支援列印任意物件以及可自定義的物件格式化器
3.支援列印陣列
4.支援列印無限長的日誌(沒有 4K 字元的限制)
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 tagthe tag of log * @param msgthe 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 logLevelthe level of log * @param tagthe tag of log * @param messagethe 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 printersthe 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 trthe 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 trthe 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 msgthe message you would like to log * @param tra 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 msgthe 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 的主要框架也基本分析完了。同時,也感謝作者無私的開源精神,向我們分享了一個如此簡單但很優秀的框架。
三、後記
感謝你能讀到並讀完此文章。希望我的分享能夠幫助到你,如果分析的過程中存在錯誤或者疑問都歡迎留言討論。