Logger使用及源码解析

在 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");

这样就可以打印日志了。

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) // (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));

再者实现对打印日志的控制:

Logger.addLogAdapter(new AndroidLogAdapter() {
@Override public boolean isLoggable(int priority, String tag) {
return BuildConfig.DEBUG;
}
});

源码解析

先看官方对工作原理的介绍:

how_it_works

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);
}
}
}
/**
* This method is synchronized in order to avoid messy of logs' order.
*/
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);
}

/**
* @return the appropriate tag based on local or global
*/
@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);

//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 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;

//corresponding method count with the current stack may exceeds the stack trace. Trims the count
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();

// 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());
}

@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);

// do nothing on the calling thread, simply pass the tag/msg to the background thread
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) { /* fail silently */ }
}
}
}

最后 Handler 接收到消息,就把日志保存到文件中。

Logger 源码分析就到这里了,作者代码写得很优雅,也非常感谢作者向开源社区贡献如此优秀的库。