安卓开发过程中,log日志是我们接触最多的一部分。如何优雅的获取log日志呢?我个人推荐使用Logger
GitHub/Logger传送门
Logger效果展示
备注:
log级别 | 颜色 |
---|---|
Verbose | BBBBBB |
Debug | 0070BB |
Info | 48BB31 |
Warm | BBBB23 |
Error | FF0006 |
Assert | 8F0005 |
控制台日志
代码部分:
截图部分:
CsvFile文件日志
代码部分
截图部分(文件保存在手机存储logger目录下)
源码分析
这些是 Logger 最基础的用法,同时还支持 xml 的打印。而且在GitHub上的README中有自定义参数的用法:
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() 方法调用了 printer.v()
Printer是接口,实现类是 LoggerPrinter,下一步进入 LoggerPrinter 类查看 v 方法的细节
LoggerPrinter.java 部分代码
private final ThreadLocal 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 用于存放标签 (给线程设置局部变量,避免出现线程并发问题,在Handler源码理解分析文末有简单的介绍),同时可以发现有 t() 方法用于值的注入并且返回 Printer 对象,这说明可以链式调用。
在 log() 方法中取出做为 tag,不过细心的可以看出在 getTag() 的时候虽然取出了 tag,但是明显取出 tag 之后就将其置空,说明tag只能使用一次
-
注意:t()方法只是给当前的线程设置一个仅能使用一次的标签参数
Logger.t("lalala").d("测试t()方法1"); Logger.d("测试t()方法2");
从上图可以看出 t() 方法可可设置 tag 标签,不过是在默认 "PRETTY_KIGGER" 之后拼上添加的 tag (细节请往下看PrettyFormatStrategy类的formatTag()方法)
回到 LoggerPrinter 的 v() 方法,它最终调用了 Printer 接口的 log() 方法,所以可以看出无论是 v,i,d,w 等方法最终都是调用 log()。当中要特别注意的是 for (LogAdapter adapter : logAdapters) 这说明了它可以配置多个 Adapter。
-
LoggerPrinter最终调用了接口 LogAdapter 的 log() 方法,所以我们要找 LogAdapter 的实现类。不过在调用之前通过 isLoggable(priority, tag) 对输出进行过滤,我们可以在初始化的时候通过重写这个方法来定义自己的规则
Logger.addLogAdapter(new AndroidLogAdapter(){ @Override public boolean isLoggable(int priority, String tag) { return super.isLoggable(priority, tag); } });
我们在README中可以看到两个实现类:
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 有两个构造方法,从无参的构造方法可以看出 PrettyFormatStrategy 使用的是 Builder设计模式。明显,无参的是我们在篇首进行测试使用的,另一个是用于自定义属性参数。
我们再进一步观察 PrettyFormatStrategy 类中发生了什么
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() 打印输出线程信息,以及调用该方法的所在代码位置
- 在 logContent() 之前先进行 message 超长处理,之后在 logContent() 中进行换行格式处理
- System.getProperty("line.separator")//换行符,功能和"\n"是一致的,但是此种写法避免了 Windows和Linux的冲突
- 最后调用接口 LogStrategy 的 log() 方法进行打印,所以我们要去寻找它的实现类 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);
}
}
- 看到这里调用了系统自带的 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 的分析,相比较这个反而会更简单一点
- 主要是对字符串的拼接,格式的调整
- 所有重点就落到了 LogSrategy 的实现类了,在 Builder 中的builder()方法中问题最大的应该是 HandlerThread 与 DiskLogStrategy了
- HandlerThread 实际上还是一个普通的Thread,不过内部实现了 Looper 循环。好处: 在子线程中实现Looper,减轻了UI线程looper的压力。如有问题可以结合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 类中有个静态内部类继承自 Handler, Looper 却是从 HandlerThread 中得到的,说明handleMessage将会在一个子线程中执行
- 通过 handleMessage 则是将 log() 方法发送的 msg 中的内容写入文件
- 个人理解:使用这种模式是为了保证日志的有序性避免多个线程对同一个文件进行编辑,且在子线程中保证不阻塞UI线程
How it works
在GitHub上,官方给出了原理图,我们分析的方向也大致如此。注意:Printer与LogAdapter的关系为1对多