Logger 使用
1. Gradle 依赖:
implementation 'com.orhanobut:logger:2.2.0'
2. 添加 Adapter
Logger.addLogAdapter(new AndroidLogAdapter());
传进来的适配器被添加到Printer中去了,Printer只是一个接口,它的实现类是LoggerPrinter。所以再来看看LoggerPrinter的addAdapter方法。
private final List<LogAdapter> logAdapters = new ArrayList<>();
...
@Override
public void addAdapter(@NonNull LogAdapter adapter) {
logAdapters.add(checkNotNull(adapter));
}
可以看到我们一开始传进来的log适配器最终被放进了一个列表里,这个列表存放了各种策略的log适配器,后面会解释它的的作用。至此,Logger初始化就结束了。
接下来就是可以直接使用了
Logger.d("message");
默认输出 Log 以及所在类名,方法名,行号,线程等相关信息。如果不想显示除 Log 外的其他信息,可以对 Adapter 进行相关设置,
FormatStrategy formatStrategy = PrettyFormatStrategy.newBuilder()
.showThreadInfo(false) // 展示线程信息
.methodCount(5) // 展示调用的方法个数,默认是 2
.methodOffset(0) // 跳过堆栈中的方法个数, 默认是 0
.tag("My custom tag") // TAG 内容. 默认是 PRETTY_LOGGER
.build();
Logger.addLogAdapter(new AndroidLogAdapter(formatStrategy));
显示如下,如果 methodOffset 为1,则不展示 MainActivity.onCreate(),只显示其他 4 个。
如果需要将日志存储到本地文件,则需要使用 DiskLogAdapter, 同时需要声明存储权限,Logger 默认生成 csv 文件,存储在 /storage/emulated/0/logger 目录下,
Logger.addLogAdapter(new DiskLogAdapter());
如果针对不同的页面 Logger 的配置不同, 可以使用 Logger.clearLogAdapters(), 然后进行重新配置。
3. 其他类型
message 除了是 String 类型,还可以是集合,数组,
Logger.t("tag").e("Custom tag for only one use");
Logger.json("{ "key": 3, "value": something}");
Logger.d(Arrays.asList("foo", "bar"));
Logger.d(new int[]{2,3,19,4});
Logger.d("");
Map<String, String> map = new HashMap<>();
map.put("key", "value");
map.put("key1", "value2");
Logger.d(map);
Logger.d("message %s", " erro");
这五个方法的实现其实区别不大,这里就以Logger.d方法为例来分析,跳进去看看。
//Logger.java
public static void d(@Nullable Object object) {
printer.d(object);
}
//LoggerPrinter.java
@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));
}
前面说过Printer的实现类是LoggerPrinter,所以实际上是调用了LoggerPrinter方法。再深入log方法看看。
/** * 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);
//1
String tag = getTag();
//2
String message = createMessage(msg, args);
//3
log(priority, tag, message, throwable);
}
注释上说使用synchronized是为了防止日志打印顺序错乱。这是合理的,毕竟可能会有多个线程同时调用这个方法。这里有三个步骤,先获取tag,看看getTag方法。
private final ThreadLocal<String> localTag = new ThreadLocal<>();
...
@Nullable private String getTag() {
String tag = localTag.get();
if (tag != null) {
localTag.remove();
return tag;
}
return null;
}
这里使用ThreadLocal了来区分不同的线程,也就是说不同的线程得到的Tag可能不一样,后面会解释原因。接着再来看createMessage方法。
@NonNull
private String createMessage(@NonNull String message, @Nullable Object... args) {
return args == null || args.length == 0 ? message : String.format(message, args);
}
可以看到当初调用Logger.d()的时候如果传入的只有字符串没有后面的参数,则直接返回这个字符串,如果传了参数就把这个字符串格式化。比如Logger.d(“num:%d”,123),这时返回的就是 “num:123” 这个字符串。最后再来看看log方法做了什么。
@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";
}
//1
for (LogAdapter adapter : logAdapters) {
if (adapter.isLoggable(priority, tag)) {
adapter.log(priority, tag, message);
}
}
}
看到代码1的for循环,上面说过,在初始化的时候我们会通过Logger.addLogAdapterf方法把log适配器添加到logAdapters这个列表中,现在将所有的适配器都遍历一遍,调用它们log方法。比如,我们添加了一个logcat适配器和文件适配器,那么调用 Logger.d(“hello world”) 后就会在logcat和文件中打印这个日志了。
Loggger还能打印json和xml,基本使用如下。
//{"name":"PYJTLK","isHandsome":false}
Logger.json("{"name":"PYJTLK","isHandsome":false}");
//<person><name>PYJTLK</name><handsome>false</handsome></person>
Logger.xml("<person><name>PYJTLK</name><handsome>false</handsome></person>");
实现非常简单,来看看代码。
@Override public void json(@Nullable String json) {
if (Utils.isEmpty(json)) {
d("Empty/Null json content");
return;
}
try {
json = json.trim();
//单个json
if (json.startsWith("{")) {
JSONObject jsonObject = new JSONObject(json);
String message = jsonObject.toString(JSON_INDENT);
d(message);
return;
}
//json数组
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");
}
}
再看看xml的解析。
@Override public void xml(@Nullable String xml) {
if (Utils.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");
}
}
临时TAG
文章开头初始化Logger后,调用Logger.d()方法使用TAG是全局的。如果要临时使用其他的TAG,调用Logger.t().d()就可以了。
Logger.t("MyTag2").d("hello world");
/** * Given tag will be used as tag only once for this method call regardless of the tag that's been * set during initialization. After this invocation, the general tag that's been set will * be used for the subsequent log calls */
public static Printer t(@Nullable String tag) {
return printer.t(tag);
}
注释上的意思是说调用这个方法传进来的TAG只会使用一次,这个方法结束后,Logger又会用回之前的全局TAG。如此一来就能做到临时变更TAG了,继续深入这个方法看看。
@Override public Printer t(String tag) {
if (tag != null) {
localTag.set(tag);
}
return this;
}
前面说过localTag是一个ThreadLocal,这里将临时TAG放了进去,接着再回顾一下上面使用到localTag的地方。
@Nullable private String getTag() {
String tag = localTag.get();
if (tag != null) {
localTag.remove();
return tag;
}
return null;
}
在打印的时候Logger就会把它取出来,然后localTag的TAG被移除,如此一来就实现了临时TAG的功能。这里需要注意,t() 和 d()是连着用的。如果先在线程A上调用t(),再到线程B调用d(),是无效的。那是因为两个线程的ThreadLocal的TAG值是不一样的
//有效,因为在同一个线程同时调用t()和d()
Logger.t("MyTag2").d("hello world");
//无效,此时线程A的ThreadLocal存着临时TAG,可线程B的ThreadLocal中存放空值
Logger.t("MyTag2");//线程A
Logger.d("hello world");//线程B
存储分析
上传客户端日志,对于分析 app 运行情况可用户使用习惯是至关重要的一步,但是 Logger 的存储路径试固定的,没有提供相关的 api 进行设置,所以可以通过分析其中的原理,然后自定义一个 adapter. 对于这里的路径我建议存储在 /Android/data/包名, 因为此路径不需要获取用户权限,可以直接使用。
通过 new DiskLogAdapter(), 在其构造方法中 CsvFormatStrategy 使用 Builder 模式 创建了一个 CsvFormatStrategy 实例, 在 builder() 可以看到具体的路径,因为一旦在子线程中操作,Handler 需要手动启动 Looper, 所以这里 通过 HandlerThread 获取 Looper 传递给 Handler, 通过 DiskLogStrategy.WriteHandler 创建 Handler, 将将文件路径传递给 DiskLogStrategy.WriteHandler, 同时通过构造方法生成 DiskLogStrategy 对象,将 handler 传递给 DiskLogStrategy, 当输出 Log 时调用 DiskLogStrategy 的 log() 方法,通过 log() 方法中 handler 将 message 发送出去, 然后交给 handler 处理,存储到 DiskLogStrategy.WriteHandler 中的路径。