1,前言
在开发过程中,log是不可缺少的助手,但直接用log,如果打印的信息过多或多个地方调用同一方法打印log,这种情况就很难定位打印出的数据是哪里调用的。就算知道哪里打印的,跳转到打印log的界面也麻烦。
像打印错误的那种定位功能就感觉很不错,点击错误的log就能跳转到调用的地点上。
网络有个打印封装Logger,就做的很不错,实现了以上功能。这里我就仿照它实现自己需要的简洁点的log打印。
2,如何实现定位日志打印
在Logger中,它是在LoggerPrinter类中的logHeaderContent方法中实现的:
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("║ ")
.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());
}
}
可以看到通过Thread.currentThread().getStackTrace()获取StackTraceElement,得到当前线程调用的栈帧集合。每调用一个方法,栈就会储存一个StackTraceElement用来储存该方法的相关信息,方法返回就出栈。通过getStackTrace()就能获得方法的调用栈,打印出文件名和行数,就能点击它定位到调用的方法了。打印出这样的信息:
MainActivity.onCreate (MainActivity.java:33)
于是仿照这个方法,简单的写出自己想要打印的格式:
private static final int MIN_STACK_OFFSET = 5; //调用MyLogger方法的最小层次
private static int logLevel = 2; //打印几个等级的方法
/**
* 打印位置,根据logLevel来打印几层的方法,默认3层
* @param logType 打印级别
* @param tag tag
*/
private static void logLocation(int logType, String tag) {
String level = "";
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
int start = getStackOffset(stackTraceElements);
for (int i = start; i < start + logLevel; i++) { //除去本身logLocation方法的打印
if (i == stackTraceElements.length) break;
StringBuilder builder = new StringBuilder();
builder.append(HORIZONTAL_DOUBLE_LINE).append(level)
.append(getSimpleClassName(stackTraceElements[i].getClassName()))
.append(".")
.append(stackTraceElements[i].getMethodName())
.append(" ")
.append(" (")
.append(stackTraceElements[i].getFileName())
.append(":")
.append(stackTraceElements[i].getLineNumber())
.append(")");
level += " ";
logChunk(logType, tag, builder.toString());
}
logLevel = 3;
}
/**
* Determines the starting index of the stack trace, after method calls made by this class.
*
* @param trace the stack trace
*
* @return the stack offset
*/
private static int getStackOffset(StackTraceElement[] trace) {
//经过debug发现,下标2开始是logLocation方法,0和1是线程方法,这个方法调用的层次是第3层 logLocation - log - d,所以MIN_STACK_OFFSET是2 + 3 = 5
//但为了可扩展,一般设为3
for (int i = MIN_STACK_OFFSET; i < trace.length; i++) {
StackTraceElement e = trace[i];
String name = e.getClassName();
if (!name.equals(MyLogger.class.getName())) {
return i;
}
}
return -1;
}
logLevel是设定打印出来的方法有几层;MIN_STACK_OFFSET是调用log打印方法在栈中的位置,0和1是线程方法,这个方法调用的层次是第3层 logLocation - log - d,所以MIN_STACK_OFFSET是2 + 3 = 5;HORIZONTAL_DOUBLE_LINE只是自己定义的打印样式'|'。
这样就实现了定位功能。主要靠获取StackTraceElement来获取调用的方法栈,再定位调用方法的位置,根据需要打印调用方法的层次。
要改变打印log的样式话,需要改变builder的拼接就行了。
打印内容:
/**
* 打印内容
* @param logType 类型
* @param tag tag
* @param chunk 内容
*/
private static void logContent(int logType, String tag, String chunk) {
String[] lines = chunk.split(System.getProperty("line.separator"));
for (String line : lines) {
logChunk(logType, tag, HORIZONTAL_DOUBLE_LINE + " " + line);
}
}
根据自己的需要设计样式,这里就是换行打印。
3,Logger的简单解析
Logger类图(来源) image
可以看出,Logger的功能实现都是通过LoggerPrinter类代理实现的。
Helper类可以说是工具类,有isEmpty,equals,getStackTraceString三个工具方法。
Settings方法,顾名思义,配置信息都通过它维护。在这个类中有个域是LogAdapter,这个接口真正的实现是AndroidLogAdapter,里面调用Log打印。
关键的实现就在LoggerPrinter里,主要的实现方法是log方法:
@Override public synchronized void log(int priority, String tag, String message, Throwable throwable) {
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;
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);
}
- logTopBorder打印顶部分割线。
- logHeaderContent打印调用方法位置用于定位。
- 如果打印内容大于可打印的最大长度CHUNK_SIZE,则拆分多段打印。logDivider打印分割线区分顶部域内容,logContent打印内容,logBottomBorder打印底部分割线。
打印方法logChunk:
private void logChunk(int logType, String tag, String chunk) {
String finalTag = formatTag(tag);
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;
}
}
它是通过Settings的LogAdapter,也就是AndroidLogAdapter调用系统Log打印的信息。
打印的Tag信息,可以通过 Logger.t(String tag)放入LoggerPrinter的localTag中
ThreadLocal<String> localTag = new ThreadLocal<>();
formatTag(tag)方法将创建LoggerPrinter时的tag拼接上通过Logger.t(String tag)的tag。最终打印出的是初始tag(Logger中的DEFAULT_TAG)加上Logger.t(String tag)中的tag。
private String formatTag(String tag) {
if (!Helper.isEmpty(tag) && !Helper.equals(this.tag, tag)) {
return this.tag + "-" + tag;
}
return this.tag;
}
定位打印通过Thread.currentThread().getStackTrace()实现,前面说过就不说了。
xml和json打印:
xml就是解析xml在打印出来:
/**
* Formats the json content and print it
*
* @param xml the xml content
*/
@Override public void xml(String xml) {
if (Helper.isEmpty(xml)) {
d("Empty/Null xml content");
return;
}
try {
Source xmlInput = new StreamSource(new StringReader(xml));
StreamResult xmlOutput = new StreamResult(new StringWriter());
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
transformer.transform(xmlInput, xmlOutput);
d(xmlOutput.getWriter().toString().replaceFirst(">", ">\n"));
} catch (TransformerException e) {
e("Invalid xml");
}
}
json判断是Array还是对象进行解析打印:
/**
* Formats the json content and print it
*
* @param json the json content
*/
@Override public void json(String json) {
if (Helper.isEmpty(json)) {
d("Empty/Null json content");
return;
}
try {
json = json.trim();
if (json.startsWith("{")) {
JSONObject jsonObject = new JSONObject(json);
String message = jsonObject.toString(JSON_INDENT);
d(message);
return;
}
if (json.startsWith("[")) {
JSONArray jsonArray = new JSONArray(json);
String message = jsonArray.toString(JSON_INDENT);
d(message);
return;
}
e("Invalid Json");
} catch (JSONException e) {
e("Invalid Json");
}
}
4,总结
Logger的功能足够满足日常开发了,各种级别打印和xml,json打印。通过仿照它,对于学习它的架构和实现方法是很不错的。
因为我需要的是普通信息打印并定位地点,而Logger打印的信息样式有些复杂,所以集成了一个类只实现少量功能。自己尝试后才发现,Logger的结构是多么好,一个类还是太臃肿了;实现细节上对于多线程的考虑Logger是充分考虑过的。
在别人的基础上学习,查看优秀的代码是很好的学习方式。
MyLogger是在一个项目中,地址。
网友评论