在 Android 开发过程中打印日志必不可少,Logger 就是一款优秀的 Android 开源日志库。
Logger 使用简单,输出日志美观高效,支持 JSON 、XML 格式输出,支持打印 Arrays 、Collections 等对象。
Logger 地址 :https://github.com/orhanobut/logger
快速入门 在项目中添加依赖:
implementation 'com.orhanobut:logger:2.2.0'
快速开始:
Logger.addLogAdapter(new AndroidLogAdapter ()); Logger.d("hello" );
这样就可以打印日志了。
当然不止打印这一种日志,和自带的函数类似:
Logger.d("debug" ); Logger.e("error" ); Logger.w("warning" ); Logger.v("verbose" ); Logger.i("information" ); Logger.wtf("What a Terrible Failure" );
也支持格式化输出:
Logger.d("hello %s" , "world" );
支持数字、集合对象的输出,注意只支持 Logger.d() 方法:
Logger.d(MAP); Logger.d(SET); Logger.d(LIST); Logger.d(ARRAY);
支持 JSON 和 XML 的输出:
Logger.json(JSON_CONTENT); Logger.xml(XML_CONTENT);
自定义 TAG ,仅本次日志打印有效:
Logger.t("MainActivity" ).d("hello" );
支持把日志保存为文件:
Logger.addLogAdapter(new DiskLogAdapter ()); Logger.d("hello" );
只需更改 LogAdapter 即可,其他使用相同。
Logger.clearLogAdapters();
清除 LogAdapter 方法。
进阶使用 利用 PrettyFormatStrategy.newBuilder 还有更高级的设置,如:
FormatStrategy formatStrategy = PrettyFormatStrategy.newBuilder() .showThreadInfo(false ) .methodCount(0 ) .methodOffset(7 ) .logStrategy(customLog) .tag("My custom tag" ) .build(); Logger.addLogAdapter(new AndroidLogAdapter (formatStrategy));
再者实现对打印日志的控制:
Logger.addLogAdapter(new AndroidLogAdapter () { @Override public boolean isLoggable (int priority, String tag) { return BuildConfig.DEBUG; } });
源码解析 先看官方对工作原理的介绍:
Logger 打印日志整个流程非常明确:
Logger 负责整个对外的接口,LoggerPrinter 负责对不同日志的整合,LogAdapter 分别对 Logcat 和 Disk 的打印进行适配,FormatStrategy 负责打印输出的配置及美化,LogStrategy 负责最终的打印输出任务。
1、Logger.java 部分代码:
private static Printer printer = new LoggerPrinter ();public static Printer t (@Nullable String tag) { return printer.t(tag); }
这里只是提供了一系列的静态函数,包括前面对不同等级的日志的输出及 JSON、XML 的输出。
实际工作还是由 LoggerPrinter 来做。
还有就是对 LogAdapter 适配器的添加清除:
public static void addLogAdapter (@NonNull LogAdapter adapter) { printer.addAdapter(checkNotNull(adapter)); } public static void clearLogAdapters () { printer.clearLogAdapters(); }
2、LoggerPrinter 部分代码:
LoggerPrinter 实现 Printer 接口,负责不同日志的整合。
@Override public void d (@NonNull String message, @Nullable Object... args) { log(DEBUG, null , message, args); } @Override public void d (@Nullable Object object) { log(DEBUG, null , Utils.toString(object)); } @Override public void e (@Nullable Throwable throwable, @NonNull String message, @Nullable Object... args) { log(ERROR, throwable, message, args); } @Override public void w (@NonNull String message, @Nullable Object... args) { log(WARN, null , message, args); }
我们可以看到这些方法都是对 log() 方法的调用。
注意这里 d() 方法可以输出任意对象,还是对象转化为字符串输出。
我们接着看 log() 方法:
private final ThreadLocal<String> localTag = new ThreadLocal <>();@Override public Printer t (String tag) { if (tag != null ) { localTag.set(tag); } return this ; } @Override public synchronized void log (int priority, @Nullable String tag, @Nullable String message, @Nullable 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); } } } private synchronized void log (int priority, @Nullable Throwable throwable, @NonNull String msg, @Nullable Object... args) { checkNotNull(msg); String tag = getTag(); String message = createMessage(msg, args); log(priority, tag, message, throwable); } @Nullable private String getTag () {String tag = localTag.get(); if (tag != null ) { localTag.remove(); return tag; } return null ; } @NonNull private String createMessage (@NonNull String message, @Nullable Object... args) { return args == null || args.length == 0 ? message : String.format(message, args); }
我们可以看到这里是交由 LogAdapter 来实现打印:
adapter.log(priority, tag, message);
在调用之前添加了 tag ,对于一次性的 tag 是由 ThreadLocal 来存储的,避免线程的并发问题,并且在取出后将其置空。
3、LogAdapter 有两个实现
AndroidLogAdapter 与 DiskLogAdapter 分别对应 Logcat 输出与 Disk 保存。
4.1、AndroidLogAdapter 部分代码:
public class AndroidLogAdapter implements LogAdapter { @NonNull private final FormatStrategy formatStrategy; public AndroidLogAdapter () { this .formatStrategy = PrettyFormatStrategy.newBuilder().build(); } public AndroidLogAdapter (@NonNull FormatStrategy formatStrategy) { this .formatStrategy = checkNotNull(formatStrategy); } @Override public boolean isLoggable (int priority, @Nullable String tag) { return true ; } @Override public void log (int priority, @Nullable String tag, @NonNull String message) { formatStrategy.log(priority, tag, message); } }
很明显只是对是否打印日志进行了控制,其他任务由 PrettyFormatStrategy 来做。
4.2、PrettyFormatStrategy 部分代码:
首先 PrettyFormatStrategy 使用 Builder 设计模式,从前面 AndroidLogAdapter 的构造函数中也可看出来。
public static class Builder { int methodCount = 2 ; int methodOffset = 0 ; boolean showThreadInfo = true ; @Nullable LogStrategy logStrategy; @Nullable String tag = "PRETTY_LOGGER" ; private Builder () { } @NonNull public Builder methodCount (int val) { methodCount = val; return this ; } @NonNull public Builder methodOffset (int val) { methodOffset = val; return this ; } @NonNull public Builder showThreadInfo (boolean val) { showThreadInfo = val; return this ; } @NonNull public Builder logStrategy (@Nullable LogStrategy val) { logStrategy = val; return this ; } @NonNull public Builder tag (@Nullable String tag) { this .tag = tag; return this ; } @NonNull public PrettyFormatStrategy build () { if (logStrategy == null ) { logStrategy = new LogcatLogStrategy (); } return new PrettyFormatStrategy (this ); } }
这样就可以对打印的日志进行配置了,也就是前面进阶使用里面的内容。
下面来看具体打印的实现:
@Override public void log (int priority, @Nullable String onceOnlyTag, @NonNull String message) { checkNotNull(message); String tag = formatTag(onceOnlyTag); logTopBorder(priority, tag); logHeaderContent(priority, tag, methodCount); 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); logContent(priority, tag, new String (bytes, i, count)); } logBottomBorder(priority, tag); } private void logTopBorder (int logType, @Nullable String tag) { logChunk(logType, tag, TOP_BORDER); } @SuppressWarnings("StringBufferReplaceableByString") private void logHeaderContent (int logType, @Nullable String tag, int methodCount) { StackTraceElement[] trace = Thread.currentThread().getStackTrace(); if (showThreadInfo) { logChunk(logType, tag, HORIZONTAL_LINE + " Thread: " + Thread.currentThread().getName()); logDivider(logType, tag); } String level = "" ; int stackOffset = getStackOffset(trace) + methodOffset; 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(HORIZONTAL_LINE) .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()); } } private void logBottomBorder (int logType, @Nullable String tag) { logChunk(logType, tag, BOTTOM_BORDER); } private void logDivider (int logType, @Nullable String tag) { logChunk(logType, tag, MIDDLE_BORDER); } private void logContent (int logType, @Nullable String tag, @NonNull String chunk) { checkNotNull(chunk); String[] lines = chunk.split(System.getProperty("line.separator" )); for (String line : lines) { logChunk(logType, tag, HORIZONTAL_LINE + " " + line); } } private void logChunk (int priority, @Nullable String tag, @NonNull String chunk) { checkNotNull(chunk); logStrategy.log(priority, tag, chunk); }
这里就是对日志美化的核心了,但是最终每一行的输出还是不是在此,是由 LogcatLogStrategy 负责的。
4.3、LogcatLogStrategy 部分代码:
LogcatLogStrategy 负责打印每一行日志,当然已经美化过了。
public class LogcatLogStrategy implements LogStrategy { static final String DEFAULT_TAG = "NO_TAG" ; @Override public void log (int priority, @Nullable String tag, @NonNull String message) { checkNotNull(message); if (tag == null ) { tag = DEFAULT_TAG; } Log.println(priority, tag, message); } }
这里调用的是系统的 Log 方法逐行打印日志。
5.1、DiskLogAdapter 部分代码:
和 AndroidLogAdapter 类似,DiskLogAdapter 也只是负责控制是否打印日志,具体工作由 CsvFormatStrategy 实现。
public class DiskLogAdapter implements LogAdapter { @NonNull private final FormatStrategy formatStrategy; public DiskLogAdapter () { formatStrategy = CsvFormatStrategy.newBuilder().build(); } public DiskLogAdapter (@NonNull FormatStrategy formatStrategy) { this .formatStrategy = checkNotNull(formatStrategy); } @Override public boolean isLoggable (int priority, @Nullable String tag) { return true ; } @Override public void log (int priority, @Nullable String tag, @NonNull String message) { formatStrategy.log(priority, tag, message); } }
5.2、CsvFormatStrategy 部分代码:
CsvFormatStrategy 处理后的日志要保存到文件,同样使用 Builder 设计模式:
我们来看下 build() 方法:
@NonNull 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 ); }
可以看到文件保存位置为 外置空间根目录 logger 文件夹下。
由 HandlerThread 启动了一个子线程,HandlerThread 实际上还是一个普通的 Thread,不过内部实现了 Looper 循环。
具体实现主要涉及字符串的拼接、格式的调整:
@Override public void log (int priority, @Nullable String onceOnlyTag, @NonNull String message) { checkNotNull(message); String tag = formatTag(onceOnlyTag); date.setTime(System.currentTimeMillis()); StringBuilder builder = new StringBuilder (); builder.append(Long.toString(date.getTime())); builder.append(SEPARATOR); builder.append(dateFormat.format(date)); builder.append(SEPARATOR); builder.append(Utils.logLevel(priority)); builder.append(SEPARATOR); builder.append(tag); if (message.contains(NEW_LINE)) { message = message.replaceAll(NEW_LINE, NEW_LINE_REPLACEMENT); } builder.append(SEPARATOR); builder.append(message); builder.append(NEW_LINE); logStrategy.log(priority, tag, builder.toString()); } @Nullable private String formatTag (@Nullable String tag) { if (!Utils.isEmpty(tag) && !Utils.equals(this .tag, tag)) { return this .tag + "-" + tag; } return this .tag; }
最终还是由 DiskLogStrategy 来负责打印输出日志。
5.3、DiskLogStrategy 部分代码:
这里使用了 Handler 实现线程间的通信:
@NonNull private final Handler handler;public DiskLogStrategy (@NonNull Handler handler) { this .handler = checkNotNull(handler); } @Override public void log (int level, @Nullable String tag, @NonNull String message) { checkNotNull(message); handler.sendMessage(handler.obtainMessage(level, message)); }
每次有新的日志,调用 handler.sendMessage() 方法。
@SuppressWarnings("checkstyle:emptyblock") @Override public void handleMessage (@NonNull 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) { } } } }
最后 Handler 接收到消息,就把日志保存到文件中。
Logger 源码分析就到这里了,作者代码写得很优雅,也非常感谢作者向开源社区贡献如此优秀的库。