在 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) // (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;
}
});
先看官方对工作原理的介绍:
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 源码分析就到这里了,作者代码写得很优雅,也非常感谢作者向开源社区贡献如此优秀的库。
评论 (0)