阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

爆料喽!!!开源日志库Logger的剖析分析

30次阅读
没有评论

共计 6479 个字符,预计需要花费 17 分钟才能阅读完成。

导读 Logger 类提供了多种方法来处理日志活动。上一篇介绍了开源日志库 Logger 的使用,今天我主要来分析 Logger 实现的原理。
库的整体架构图

爆料喽!!!开源日志库 Logger 的剖析分析

详细剖析

我们从使用的角度来对 Logger 库抽茧剥丝:

String userName = "Jerry";
     Logger.i(userName);

看看 Logger.i() 这个方法:

public static void i(String message, Object... args) {printer.i(message, args);
            }

还有个可变参数,来看看 printer.i(message, args) 是啥:

public Interface Printer{void i(String message, Object... args);
               }

是个接口,那我们就要找到这个接口的实现类,找到 printer 对象在 Logger 类中声明的地方:

private static Printer printer = new LoggerPrinter();

实现类是 LoggerPrinter,而且这还是个静态的成员变量,这个静态是有用处的,后面会讲到,那就继续跟踪 LoggerPrinter 类的 i(String message, Object… args) 方法的实现:

@Override public void i(String message, Object... args) {log(INFO, null, message, args);
}
/**
* This method is synchronized in order to avoid messy of logs' order.
*/
private synchronized void log(int priority, Throwable throwable, String msg, Object... args) {
// 判断当前设置的日志级别,为 NONE 则不打印日志
if (settings.getLogLevel() == LogLevel.NONE) {return;}
// 获取 tag
String tag = getTag();
// 创建打印的消息
String message = createMessage(msg, args);
// 打印
log(priority, tag, message, throwable);
}
public enum LogLevel {
/**
* Prints all logs
*/
FULL,
/**
* No log will be printed
*/
NONE
}

首先,log 方法是一个线程安全的同步方法,为了防止日志打印时候顺序的错乱,在多线程环境下,这是非常有必要的。
其次,判断日志配置的打印级别,FULL 打印全部日志,NONE 不打印日志。
再来,getTag():

private final ThreadLocal<String> localTag = new ThreadLocal<>();
/**
* @return the appropriate tag based on local or global */
private String getTag() {
// 从 ThreadLocal<String> localTag 里获取本地一个缓存的 tag
String tag = localTag.get();
if (tag != null) {localTag.remove();
return tag;
}
return this.tag;
}

这个方法是获取本地或者全局的 tag 值,当 localTag 中有 tag 的时候就返回出去,并且清空 localTag 的值,关于 ThreadLocal 还不是很清楚的可以参考主席的文章:http://blog.csdn.net/singwhat…

接着,createMessage 方法:

private String createMessage(String message, Object... args) {return args == null || args.length == 0 ? message : String.format(message, args);
}

这里就很清楚了,为什么我们用 Logger.i(message, args) 的时候没有写 args,也就是 null,也可以打印,而且是直接打印的 message 消息的原因。同样博主上一篇文章也提到了:

Logger.i("博主今年才 %d,英文名是 %s", 16, "Jerry");

像这样的可以拼接不同格式的数据的打印日志,原来实现的方式是用 String.format 方法,这个想必小伙伴们在开发 Android 应用的时候 String.xml 里的动态字符占位符用的也不少,应该很容易理解这个 format 方法的用法。

重头戏,我们把 tag,打印级别,打印的消息处理好了,接下来该打印出来了:

@Override public synchronized void log(int priority, String tag, String message, Throwable throwable) {
// 同样判断一次库配置的打印开关,为 NONE 则不打印日志
if (settings.getLogLevel() == LogLevel.NONE) {return;}
// 异常和消息不为空的时候,获取异常的原因转换成字符串后拼接到打印的消息中
if (throwable != null && message != null) {message += ":" + Helper.getStackTraceString(throwable);
}
if (throwable != null && message == null) {message = Helper.getStackTraceString(throwable);
}
if (message == null) {message = "No message/exception is set";}
// 获取方法数
int methodCount = getMethodCount();
// 判断消息是否为空
if (Helper.isEmpty(message)) {message = "Empty/NULL log message";}
// 打印日志体的上边界
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;
// 消息字节长度小于等于 4000
if (length <= CHUNK_SIZE) {if (methodCount > 0) {
// 方法数大于 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);
}

我们重点来看看 logHeaderContent 方法和 logContent 方法:

@SuppressWarnings("StringBufferReplaceableByString")
private void logHeaderContent(int logType, String tag, int methodCount) {
// 获取当前线程堆栈跟踪元素数组
//(里面存储了虚拟机调用的方法的一些信息:方法名、类名、调用此方法在文件中的行数)// 这也是这个库的“核心”StackTraceElement[] trace = Thread.currentThread().getStackTrace();
// 判断库的配置是否显示线程信息
if (settings.isShowThreadInfo()) {
// 获取当前线程的名称,并且打印出来,然后打印分割线
logChunk(logType, tag, HORIZONTAL_DOUBLE_LINE + "Thread:" + Thread.currentThread().getName());    logDivider(logType, tag);
}
String level = "";
// 获取追踪栈的方法起始位置
int stackOffset = getStackOffset(trace) + settings.getMethodOffset();
//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("U")
.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());
}
}

爆料喽!!!开源日志库 Logger 的剖析分析

接下来看 logContent 方法:

private void logContent(int logType, String tag, String chunk) {
// 这个作用就是获取换行符数组,getProperty 方法获取的就是 "//n" 的意思
String[] lines = chunk.split(System.getProperty("line.separator"));
for (String line : lines) {
// 打印出包含换行符的内容
logChunk(logType, tag, HORIZONTAL_DOUBLE_LINE + " " + line);
}
}

如上图来说内容是字符串数组,本身里面是没用换行符的,所以不需要换行,打印出来的效果就是一行,但是 json、xml 这样的格式是有换行符的,所以打印呈现出来的效果就是:

爆料喽!!!开源日志库 Logger 的剖析分析

上面说了大半天,都还没看到具体的打印是啥,现在来看看 logChunk 方法:

private void logChunk(int logType, String tag, String chunk) {
// 最后格式化下 tag
String finalTag = formatTag(tag);
// 根据不同的日志打印类型,然后交给 LogAdapter 这个接口来打印
switch (logType) {
case ERROR:
settings.getLogAdapter().e(finalTag, chunk);
break;
case INFO:
settings.getLogAdapter().i(finalTag, chunk);
break;
case VERBOSE:
settings.getLogAdapter().v(finalTag, chunk);
break;
case WARN:
settings.getLogAdapter().w(finalTag, chunk);
break;
case ASSERT:
settings.getLogAdapter().wtf(finalTag, chunk);
break;
case DEBUG:
// Fall through, log debug by default
default:
settings.getLogAdapter().d(finalTag, chunk);
break;
}
}

这个方法很简单,就是最后格式化 tag,然后根据不同的日志类型把打印的工作交给 LogAdapter 接口来处理,我们来看看 settings.getLogAdapter() 这个方法 (Settings.java 文件):

public LogAdapter getLogAdapter() {if (logAdapter == null) {
// 最终的实现类是 AndroidLogAdapter
logAdapter = new AndroidLogAdapter();}
return logAdapter;
}

找到 AndroidLogAdapter 类:

爆料喽!!!开源日志库 Logger 的剖析分析

原来绕了一大圈,最终打印还是使用了:系统的 Log。

好了 Logger 日志框架的源码解析完了,有没有更清晰呢,也许小伙伴会说这个最终的日志打印,我不想用系统的 Log,是不是可以换呢。这是自然的,看开篇的那种整体架构图,这个 LogAdapter 是个接口,只要实现这个接口,里面做你自己想要打印的方式,然后通过 Settings 的 logAdapter(LogAdapter logAdapter) 方法设置进去就可以。

以上就是博主分析一个开源库的思路,从使用的角度出发抽茧剥丝,基本上一个库的核心部分都能搞懂。画画整个框架的大概类图,对分析库非常有帮助,每一个轮子都有值得学习的地方,吸收了就是进步的开始,耐心的分析完一个库,还是非常有成就感的。

阿里云 2 核 2G 服务器 3M 带宽 61 元 1 年,有高配

腾讯云新客低至 82 元 / 年,老客户 99 元 / 年

代金券:在阿里云专用满减优惠券

正文完
星哥说事-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2024-07-24发表,共计6479字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中