開源日志庫Logger的剖析
上一篇介紹了開源日志庫Logger的使用,今天主要來分析Logger實現(xiàn)的原理。
庫的整體架構圖
詳細剖析
我們從使用的角度來對Logger庫抽繭剝絲:
- String userName = "Jerry";
- Logger.i(userName);
看看Logger.i()這個方法:
- public static void i(String message, Object... args) {
- printer.i(message, args);
- }
還有個可變參數(shù),來看看printer.i(message, args)是啥:
- public Interface Printer{
- void i(String message, Object... args);
- }
是個接口,那我們就要找到這個接口的實現(xiàn)類,找到printer對象在Logger類中聲明的地方:
- private static Printer printer = new LoggerPrinter();
實現(xiàn)類是LoggerPrinter,而且這還是個靜態(tài)的成員變量,這個靜態(tài)是有用處的,后面會講到,那就繼續(xù)跟蹤LoggerPrinter類的i(String message, Object... args)方法的實現(xiàn):
- @Override public void i(String message, Object... args) {
- log(INFO, null, message, args);
- }
- /**
- * This method is synchronized in order to avoid messy of logs' order.
- */
- private synchronized void log(int priority, Throwable throwable, String msg, Object... args) {
- // 判斷當前設置的日志級別,為NONE則不打印日志
- if (settings.getLogLevel() == LogLevel.NONE) {
- return;
- }
- // 獲取tag
- String tag = getTag();
- // 創(chuàng)建打印的消息
- String message = createMessage(msg, args);
- // 打印
- log(priority, tag, message, throwable);
- }
- public enum LogLevel {
- /**
- * Prints all logs
- */
- FULL,
- /**
- * No log will be printed
- */
- NONE
- }
- 首先,log方法是一個線程安全的同步方法,為了防止日志打印時候順序的錯亂,在多線程環(huán)境下,這是非常有必要的。
- 其次,判斷日志配置的打印級別,F(xiàn)ULL打印全部日志,NONE不打印日志。
- 再來,getTag():
- private final ThreadLocal<String> localTag = new ThreadLocal<>();
- /**
- * @return the appropriate tag based on local or global */
- private String getTag() {
- // 從ThreadLocal<String> localTag里獲取本地一個緩存的tag
- String tag = localTag.get();
- if (tag != null) {
- localTag.remove();
- return tag;
- }
- return this.tag;
- }
這個方法是獲取本地或者全局的tag值,當localTag中有tag的時候就返回出去,并且清空localTag的值,關于ThreadLocal還不是很清楚的可以參考主席的文章:http://blog.csdn.net/singwhat...
接著,createMessage方法:
- private String createMessage(String message, Object... args) {
- return args == null || args.length == 0 ? message : String.format(message, args);
- }
這里就很清楚了,為什么我們用Logger.i(message, args)的時候沒有寫args,也就是null,也可以打印,而且是直接打印的message消息的原因。同樣博主上一篇文章也提到了:
- Logger.i("博主今年才%d,英文名是%s", 16, "Jerry");
像這樣的可以拼接不同格式的數(shù)據的打印日志,原來實現(xiàn)的方式是用String.format方法,這個想必小伙伴們在開發(fā)Android應用的時候String.xml里的動態(tài)字符占位符用的也不少,應該很容易理解這個format方法的用法。
重頭戲,我們把tag,打印級別,打印的消息處理好了,接下來該打印出來了:
- @Override public synchronized void log(int priority, String tag, String message, Throwable throwable) {
- // 同樣判斷一次庫配置的打印開關,為NONE則不打印日志
- if (settings.getLogLevel() == LogLevel.NONE) {
- return;
- }
- // 異常和消息不為空的時候,獲取異常的原因轉換成字符串后拼接到打印的消息中
- if (throwable != null && message != null) {
- message += " : " + Helper.getStackTraceString(throwable);
- }
- if (throwable != null && message == null) {
- message = Helper.getStackTraceString(throwable);
- }
- if (message == null) {
- message = "No message/exception is set";
- }
- // 獲取方法數(shù)
- int methodCount = getMethodCount();
- // 判斷消息是否為空
- if (Helper.isEmpty(message)) {
- message = "Empty/NULL log message";
- }
- // 打印日志體的上邊界
- logTopBorder(priority, tag);
- // 打印日志體的頭部內容
- logHeaderContent(priority, tag, methodCount);
- //get bytes of message with system's default charset (which is UTF-8 for Android)
- byte[] bytes = message.getBytes();
- int length = bytes.length;
- // 消息字節(jié)長度小于等于4000
- if (length <= CHUNK_SIZE) {
- if (methodCount > 0) {
- // 方法數(shù)大于0,打印出分割線
- logDivider(priority, tag);
- }
- // 打印消息內容
- logContent(priority, tag, message);
- // 打印日志體底部邊界
- logBottomBorder(priority, tag);
- return;
- }
- if (methodCount > 0) {
- logDivider(priority, tag);
- }
- for (int i = 0; i < length; i += CHUNK_SIZE) {
- int count = Math.min(length - i, CHUNK_SIZE);
- //create a new String with system's default charset (which is UTF-8 for Android)
- logContent(priority, tag, new String(bytes, i, count));
- }
- logBottomBorder(priority, tag);
- }
我們重點來看看logHeaderContent方法和logContent方法:
- @SuppressWarnings("StringBufferReplaceableByString")
- private void logHeaderContent(int logType, String tag, int methodCount) {
- // 獲取當前線程堆棧跟蹤元素數(shù)組
- //(里面存儲了虛擬機調用的方法的一些信息:方法名、類名、調用此方法在文件中的行數(shù))
- // 這也是這個庫的 “核心”
- StackTraceElement[] trace = Thread.currentThread().getStackTrace();
- // 判斷庫的配置是否顯示線程信息
- if (settings.isShowThreadInfo()) {
- // 獲取當前線程的名稱,并且打印出來,然后打印分割線
- logChunk(logType, tag, HORIZONTAL_DOUBLE_LINE + "Thread: " + Thread.currentThread().getName()); logDivider(logType, tag);
- }
- String level = "";
- // 獲取追蹤棧的方法起始位置
- int stackOffset = getStackOffset(trace) + settings.getMethodOffset();
- //corresponding method count with the current stack may exceeds the stack trace. Trims the count
- // 打印追蹤的方法數(shù)超過了當前線程能夠追蹤的方法數(shù),總的追蹤方法數(shù)扣除偏移量(從調用日志的起算扣除的方法數(shù)),就是需要打印的方法數(shù)量
- if (methodCount + stackOffset > trace.length) {
- methodCount = trace.length - stackOffset - 1;
- }
- for (int i = methodCount; i > 0; i--) {
- int stackIndex = i + stackOffset;
- if (stackIndex >= trace.length) {
- continue;
- }
- // 拼接方法堆棧調用路徑追蹤字符串
- StringBuilder builder = new StringBuilder();
- builder.append("║ ")
- .append(level)
- .append(getSimpleClassName(trace[stackIndex].getClassName())) // 追蹤到的類名
- .append(".")
- .append(trace[stackIndex].getMethodName()) // 追蹤到的方法名
- .append(" ")
- .append(" (")
- .append(trace[stackIndex].getFileName()) // 方法所在的文件名
- .append(":")
- .append(trace[stackIndex].getLineNumber()) // 在文件中的行號
- .append(")");
- level += " ";
- // 打印出頭部信息
- logChunk(logType, tag, builder.toString());
- }
- }
接下來看logContent方法:
- private void logContent(int logType, String tag, String chunk) {
- // 這個作用就是獲取換行符數(shù)組,getProperty方法獲取的就是"\\n"的意思
- String[] lines = chunk.split(System.getProperty("line.separator"));
- for (String line : lines) {
- // 打印出包含換行符的內容
- logChunk(logType, tag, HORIZONTAL_DOUBLE_LINE + " " + line);
- }
- }
如上圖來說內容是字符串數(shù)組,本身里面是沒用換行符的,所以不需要換行,打印出來的效果就是一行,但是json、xml這樣的格式是有換行符的,所以打印呈現(xiàn)出來的效果就是:
上面說了大半天,都還沒看到具體的打印是啥,現(xiàn)在來看看logChunk方法:
- private void logChunk(int logType, String tag, String chunk) {
- // ***格式化下tag
- String finalTag = formatTag(tag);
- // 根據不同的日志打印類型,然后交給LogAdapter這個接口來打印
- switch (logType) {
- case ERROR:
- settings.getLogAdapter().e(finalTag, chunk);
- break;
- case INFO:
- settings.getLogAdapter().i(finalTag, chunk);
- break;
- case VERBOSE:
- settings.getLogAdapter().v(finalTag, chunk);
- break;
- case WARN:
- settings.getLogAdapter().w(finalTag, chunk);
- break;
- case ASSERT:
- settings.getLogAdapter().wtf(finalTag, chunk);
- break;
- case DEBUG:
- // Fall through, log debug by default
- default:
- settings.getLogAdapter().d(finalTag, chunk);
- break;
- }
- }
這個方法很簡單,就是***格式化tag,然后根據不同的日志類型把打印的工作交給LogAdapter接口來處理,我們來看看settings.getLogAdapter()這個方法(Settings.java文件):
- public LogAdapter getLogAdapter() {
- if (logAdapter == null) {
- // 最終的實現(xiàn)類是AndroidLogAdapter
- logAdapter = new AndroidLogAdapter();
- }
- return logAdapter;
- }
找到AndroidLogAdapter類:
原來繞了一大圈,最終打印還是使用了:系統(tǒng)的Log。
好了Logger日志框架的源碼解析完了,有沒有更清晰呢,也許小伙伴會說這個最終的日志打印,我不想用系統(tǒng)的Log,是不是可以換呢。這是自然的,看開篇的那種整體架構圖,這個LogAdapter是個接口,只要實現(xiàn)這個接口,里面做你自己想要打印的方式,然后通過Settings 的logAdapter(LogAdapter logAdapter)方法設置進去就可以。
以上就是博主分析一個開源庫的思路,從使用的角度出發(fā)抽繭剝絲,基本上一個庫的核心部分都能搞懂。畫畫整個框架的大概類圖,對分析庫非常有幫助,每一個輪子都有值得學習的地方,吸收了就是進步的開始,耐心的分析完一個庫,還是非常有成就感的。
感謝你耐心看完,以后博主還會繼續(xù)努力分析其它輪子的。