Android 日志框架Logger使用教程及源码分析

1,065 阅读3分钟

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 中的路径。