安卓開發(fā)過程中,log日志是我們接觸最多的一部分星持。如何優(yōu)雅的獲取log日志呢抢埋?我個人推薦使用Logger
Logger效果展示
備注:
log級別 | 顏色 |
---|---|
Verbose | BBBBBB |
Debug | 0070BB |
Info | 48BB31 |
Warm | BBBB23 |
Error | FF0006 |
Assert | 8F0005 |
控制臺日志
代碼部分:
截圖部分:
CsvFile文件日志
代碼部分
截圖部分(文件保存在手機存儲logger目錄下)
源碼分析
這些是 Logger 最基礎(chǔ)的用法,同時還支持 xml 的打印督暂。而且在GitHub上的README中有自定義參數(shù)的用法:
FormatStrategy formatStrategy = PrettyFormatStrategy.newBuilder()
.showThreadInfo(false) // (Optional) Whether to show thread info or not. Default true
.methodCount(0) // (Optional) How many method line to show. Default 2
.methodOffset(7) // (Optional) Hides internal method calls up to offset. Default 5
.logStrategy(customLog) // (Optional) Changes the log strategy to print out. Default LogCat
.tag("My custom tag") // (Optional) Global tag for every log. Default PRETTY_LOGGER
.build();
Logger.addLogAdapter(new AndroidLogAdapter(formatStrategy));
不過揪垄,我們先從就從 v 方法開始分析:
Logger.java 部分代碼
private static Printer printer = new LoggerPrinter();
public static void v(String message, Object... args) {
printer.v(message, args);
}
首先,我們進入 Logger 類中,看到 v() 方法調(diào)用了 printer.v()
Printer是接口逻翁,實現(xiàn)類是 LoggerPrinter,下一步進入 LoggerPrinter 類查看 v 方法的細節(jié)
LoggerPrinter.java 部分代碼
private final ThreadLocal<String> localTag = new ThreadLocal<>();
@Override
public Printer t(String tag) {
if (tag != null) {
localTag.set(tag);
}
return this;
}
@Override
public void v(String message, Object... args) {
log(VERBOSE, null, message, args);
}
private synchronized void log(int priority, Throwable throwable, String msg, Object... args) {
String tag = getTag();
String message = createMessage(msg, args);
log(priority, tag, message, throwable);
}
private String getTag() {
String tag = localTag.get();
if (tag != null) {
localTag.remove();
return tag;
}
return null;
}
@Override
public synchronized void log(int priority, String tag, String message, Throwable throwable) {
if (throwable != null && message != null) {
message += " : " + Utils.getStackTraceString(throwable);
}
if (throwable != null && message == null) {
message = Utils.getStackTraceString(throwable);
}
if (Utils.isEmpty(message)) {
message = "Empty/NULL log message";
}
for (LogAdapter adapter : logAdapters) {
if (adapter.isLoggable(priority, tag)) {
adapter.log(priority, tag, message);
}
}
}
從上面的代碼可以看出 LoggerPrinter 類中有一個ThreadLocal 用于存放標簽 (給線程設(shè)置局部變量,避免出現(xiàn)線程并發(fā)問題,在Handler源碼理解分析文末有簡單的介紹)饥努,同時可以發(fā)現(xiàn)有 t() 方法用于值的注入并且返回 Printer 對象,這說明可以鏈式調(diào)用八回。
在 log() 方法中取出做為 tag酷愧,不過細心的可以看出在 getTag() 的時候雖然取出了 tag驾诈,但是明顯取出 tag 之后就將其置空,說明tag只能使用一次
-
注意:t()方法只是給當前的線程設(shè)置一個僅能使用一次的標簽參數(shù)
Logger.t("lalala").d("測試t()方法1"); Logger.d("測試t()方法2");
從上圖可以看出 t() 方法可可設(shè)置 tag 標簽溶浴,不過是在默認 "PRETTY_KIGGER" 之后拼上添加的 tag (細節(jié)請往下看PrettyFormatStrategy類的formatTag()方法)
回到 LoggerPrinter 的 v() 方法翘鸭,它最終調(diào)用了 Printer 接口的 log() 方法,所以可以看出無論是 v,i,d,w 等方法最終都是調(diào)用 log()戳葵。當中要特別注意的是 for (LogAdapter adapter : logAdapters) 這說明了它可以配置多個 Adapter就乓。
-
LoggerPrinter最終調(diào)用了接口 LogAdapter 的 log() 方法,所以我們要找 LogAdapter 的實現(xiàn)類拱烁。不過在調(diào)用之前通過 isLoggable(priority, tag) 對輸出進行過濾生蚁,我們可以在初始化的時候通過重寫這個方法來定義自己的規(guī)則
Logger.addLogAdapter(new AndroidLogAdapter(){ @Override public boolean isLoggable(int priority, String tag) { return super.isLoggable(priority, tag); } });
我們在README中可以看到兩個實現(xiàn)類:
AndroidLogAdapter 與 DiskLogAdapter
下面我們分別對它們進行分析
AndroidLogAdapter.java代碼
public class AndroidLogAdapter implements LogAdapter {
private final FormatStrategy formatStrategy;
public AndroidLogAdapter() {
this.formatStrategy = PrettyFormatStrategy.newBuilder().build();
}
public AndroidLogAdapter(FormatStrategy formatStrategy) {
this.formatStrategy = formatStrategy;
}
@Override public boolean isLoggable(int priority, String tag) {
return true;
}
@Override public void log(int priority, String tag, String message) {
formatStrategy.log(priority, tag, message);
}
}
可以看出 AndroidLogAdapter 有兩個構(gòu)造方法,從無參的構(gòu)造方法可以看出 PrettyFormatStrategy 使用的是 Builder設(shè)計模式戏自。明顯邦投,無參的是我們在篇首進行測試使用的,另一個是用于自定義屬性參數(shù)擅笔。
我們再進一步觀察 PrettyFormatStrategy 類中發(fā)生了什么
PrettyFormatStrategy.java部分代碼
private final int methodCount;
private final int methodOffset;
private final boolean showThreadInfo;
private final LogStrategy logStrategy;
private final String tag;
@Override public void log(int priority, String onceOnlyTag, String message) {
String tag = formatTag(onceOnlyTag);
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;
if (length <= CHUNK_SIZE) {
if (methodCount > 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);
}
private String formatTag(String tag) {
if (!Utils.isEmpty(tag) && !Utils.equals(this.tag, tag)) {
return this.tag + "-" + tag;
}
return this.tag;
}
private void logContent(int logType, String tag, String chunk) {
String[] lines = chunk.split(System.getProperty("line.separator"));
for (String line : lines) {
logChunk(logType, tag, HORIZONTAL_LINE + " " + line);
}
}
private void logChunk(int priority, String tag, String chunk) {
logStrategy.log(priority, tag, chunk);
}
- 在 log() 方法中開始通過 formatTag() 拼接添加的 tag 標簽
- 通過 logTopBorder() 打印輸出的頭部邊界
- 通過 logHeaderContent() 打印輸出線程信息志衣,以及調(diào)用該方法的所在代碼位置
- 在 logContent() 之前先進行 message 超長處理,之后在 logContent() 中進行換行格式處理
- System.getProperty("line.separator")//換行符,功能和"\n"是一致的,但是此種寫法避免了 Windows和Linux的沖突
- 最后調(diào)用接口 LogStrategy 的 log() 方法進行打印猛们,所以我們要去尋找它的實現(xiàn)類 LogcatLogStrategy
- 明顯念脯,我們可以自定義打印的策略,通過Builder傳入弯淘,否則將使用默認的 LogcatLogStrategy
LogcatLogStrategy.java
public class LogcatLogStrategy implements LogStrategy {
@Override public void log(int priority, String tag, String message) {
Log.println(priority, tag, message);
}
}
- 看到這里調(diào)用了系統(tǒng)自帶的 Log 來打印
DiskLogAdapter.java部分代碼
public class DiskLogAdapter implements LogAdapter {
private final FormatStrategy formatStrategy;
public DiskLogAdapter() {
formatStrategy = CsvFormatStrategy.newBuilder().build();
}
public DiskLogAdapter(FormatStrategy formatStrategy) {
this.formatStrategy = formatStrategy;
}
@Override public boolean isLoggable(int priority, String tag) {
return true;
}
@Override public void log(int priority, String tag, String message) {
formatStrategy.log(priority, tag, message);
}
}
- 與 AndroidLogAdapter 一樣我們直接分析分析 CsvFormatStrategy 類
CsvFormatStrategy.java部分代碼
private final Date date;
private final SimpleDateFormat dateFormat;
private final LogStrategy logStrategy;
private final String tag;
private CsvFormatStrategy(Builder builder) {
date = builder.date;
dateFormat = builder.dateFormat;
logStrategy = builder.logStrategy;
tag = builder.tag;
}
public static Builder newBuilder() {
return new Builder();
}
@Override public void log(int priority, String onceOnlyTag, String message) {
String tag = formatTag(onceOnlyTag);
date.setTime(System.currentTimeMillis());
StringBuilder builder = new StringBuilder();
// machine-readable date/time
builder.append(Long.toString(date.getTime()));
// human-readable date/time
builder.append(SEPARATOR);
builder.append(dateFormat.format(date));
// level
builder.append(SEPARATOR);
builder.append(Utils.logLevel(priority));
// tag
builder.append(SEPARATOR);
builder.append(tag);
// message
if (message.contains(NEW_LINE)) {
// a new line would break the CSV format, so we replace it here
message = message.replaceAll(NEW_LINE, NEW_LINE_REPLACEMENT);
}
builder.append(SEPARATOR);
builder.append(message);
// new line
builder.append(NEW_LINE);
logStrategy.log(priority, tag, builder.toString());
}
public static final class Builder {
.
.
.
public CsvFormatStrategy build() {
if (date == null) {
date = new Date();
}
if (dateFormat == null) {
dateFormat = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss.SSS", Locale.UK);
}
if (logStrategy == null) {
String diskPath = Environment.getExternalStorageDirectory().getAbsolutePath();
String folder = diskPath + File.separatorChar + "logger";
HandlerThread ht = new HandlerThread("AndroidFileLogger." + folder);
ht.start();
Handler handler = new DiskLogStrategy.WriteHandler(ht.getLooper(), folder, MAX_BYTES);
logStrategy = new DiskLogStrategy(handler);
}
return new CsvFormatStrategy(this);
}
}
- 有了 PrettyFormatStrategy 的分析绿店,相比較這個反而會更簡單一點
- 主要是對字符串的拼接,格式的調(diào)整
- 所有重點就落到了 LogSrategy 的實現(xiàn)類了,在 Builder 中的builder()方法中問題最大的應(yīng)該是 HandlerThread 與 DiskLogStrategy了
- HandlerThread 實際上還是一個普通的Thread庐橙,不過內(nèi)部實現(xiàn)了 Looper 循環(huán)假勿。好處: 在子線程中實現(xiàn)Looper,減輕了UI線程looper的壓力态鳖。如有問題可以結(jié)合Handler源碼理解分析進行理解
- 并且可以看出日志文件保存在 Environment.getExternalStorageDirectory().getAbsolutePath() + "logger" 文件夾下
- 接下來我們開始分析 DiskLogStrategy
DiskLogStrategy.java部分代碼
private final Handler handler;
public DiskLogStrategy(Handler handler) {
this.handler = handler;
}
@Override public void log(int level, String tag, String message) {
// do nothing on the calling thread, simply pass the tag/msg to the background thread
handler.sendMessage(handler.obtainMessage(level, message));
}
static class WriteHandler extends Handler {
private final String folder;
private final int maxFileSize;
WriteHandler(Looper looper, String folder, int maxFileSize) {
super(looper);
this.folder = folder;
this.maxFileSize = maxFileSize;
}
@SuppressWarnings("checkstyle:emptyblock")
@Override public void handleMessage(Message msg) {
String content = (String) msg.obj;
FileWriter fileWriter = null;
File logFile = getLogFile(folder, "logs");
try {
fileWriter = new FileWriter(logFile, true);
writeLog(fileWriter, content);
fileWriter.flush();
fileWriter.close();
} catch (IOException e) {
if (fileWriter != null) {
try {
fileWriter.flush();
fileWriter.close();
} catch (IOException e1) { /* fail silently */ }
}
}
}
/**
* This is always called on a single background thread.
* Implementing classes must ONLY write to the fileWriter and nothing more.
* The abstract class takes care of everything else including close the stream and catching IOException
*
* @param fileWriter an instance of FileWriter already initialised to the correct file
*/
private void writeLog(FileWriter fileWriter, String content) throws IOException {
fileWriter.append(content);
}
private File getLogFile(String folderName, String fileName) {
.
.
.
return newFile;
}
- 從上面的代碼可以看出 DiskLogStrategy 類中有個靜態(tài)內(nèi)部類繼承自 Handler, Looper 卻是從 HandlerThread 中得到的转培,說明handleMessage將會在一個子線程中執(zhí)行
- 通過 handleMessage 則是將 log() 方法發(fā)送的 msg 中的內(nèi)容寫入文件
- 個人理解:使用這種模式是為了保證日志的有序性避免多個線程對同一個文件進行編輯,且在子線程中保證不阻塞UI線程
How it works
在GitHub上浸须,官方給出了原理圖兆蕉,我們分析的方向也大致如此羽戒。注意:Printer與LogAdapter的關(guān)系為1對多