Logger使用及源码解析

Author Avatar
wshunli 5月 22, 2018
  • 在其它设备中阅读本文章

在 Android 开发过程中打印日志必不可少,Logger 就是一款优秀的 Android 开源日志库。

Logger 使用简单,输出日志美观高效,支持 JSON 、XML 格式输出,支持打印 Arrays 、Collections 等对象。

Logger 地址 :https://github.com/orhanobut/logger

快速入门

在项目中添加依赖:

implementation 'com.orhanobut:logger:2.2.0'

快速开始:

Logger.addLogAdapter(new AndroidLogAdapter());
Logger.d("hello");

这样就可以打印日志了。

hello

当然不止打印这一种日志,和自带的函数类似:

Logger.d("debug");
Logger.e("error");
Logger.w("warning");
Logger.v("verbose");
Logger.i("information");
Logger.wtf("What a Terrible Failure");

也支持格式化输出:

Logger.d("hello %s", "world");

支持数字、集合对象的输出,注意只支持 Logger.d() 方法:

Logger.d(MAP);
Logger.d(SET);
Logger.d(LIST);
Logger.d(ARRAY);

支持 JSON 和 XML 的输出:

Logger.json(JSON_CONTENT);
Logger.xml(XML_CONTENT);

自定义 TAG ,仅本次日志打印有效:

Logger.t("MainActivity").d("hello");

支持把日志保存为文件:

Logger.addLogAdapter(new DiskLogAdapter());
Logger.d("hello");

只需更改 LogAdapter 即可,其他使用相同。

Logger.clearLogAdapters();

清除 LogAdapter 方法。

进阶使用

利用 PrettyFormatStrategy.newBuilder 还有更高级的设置,如:

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));

再者实现对打印日志的控制:

Logger.addLogAdapter(new AndroidLogAdapter() {
  @Override public boolean isLoggable(int priority, String tag) {
    return BuildConfig.DEBUG;
  }
});

源码解析

先看官方对工作原理的介绍:

how_it_works

Logger 打印日志整个流程非常明确:

Logger 负责整个对外的接口,LoggerPrinter 负责对不同日志的整合,LogAdapter 分别对 Logcat 和 Disk 的打印进行适配,FormatStrategy 负责打印输出的配置及美化,LogStrategy 负责最终的打印输出任务。

1、Logger.java 部分代码:

private static Printer printer = new LoggerPrinter();
public static Printer t(@Nullable String tag) {
    return printer.t(tag);
}

这里只是提供了一系列的静态函数,包括前面对不同等级的日志的输出及 JSON、XML 的输出。

实际工作还是由 LoggerPrinter 来做。

还有就是对 LogAdapter 适配器的添加清除:

public static void addLogAdapter(@NonNull LogAdapter adapter) {
    printer.addAdapter(checkNotNull(adapter));
}

public static void clearLogAdapters() {
    printer.clearLogAdapters();
}

2、LoggerPrinter 部分代码:

LoggerPrinter 实现 Printer 接口,负责不同日志的整合。

@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));
}
@Override public void e(@Nullable Throwable throwable, @NonNull String message, @Nullable Object... args) {
    log(ERROR, throwable, message, args);
}
@Override public void w(@NonNull String message, @Nullable Object... args) {
    log(WARN, null, message, args);
}

我们可以看到这些方法都是对 log() 方法的调用。

注意这里 d() 方法可以输出任意对象,还是对象转化为字符串输出。

我们接着看 log() 方法:

private final ThreadLocal<String> localTag = new ThreadLocal<>();

@Override public Printer t(String tag) {
    if (tag != null) {
        localTag.set(tag);
    }
    return this;
}
@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";
    }

    for (LogAdapter adapter : logAdapters) {
        if (adapter.isLoggable(priority, tag)) {
        adapter.log(priority, tag, message);
        }
    }
}
/**
* 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);

    String tag = getTag();
    String message = createMessage(msg, args);
    log(priority, tag, message, throwable);
}

/**
* @return the appropriate tag based on local or global
*/
@Nullable private String getTag() {
String tag = localTag.get();
    if (tag != null) {
        localTag.remove();
        return tag;
    }
    return null;
}

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

我们可以看到这里是交由 LogAdapter 来实现打印:

adapter.log(priority, tag, message);

在调用之前添加了 tag ,对于一次性的 tag 是由 ThreadLocal 来存储的,避免线程的并发问题,并且在取出后将其置空。

3、LogAdapter 有两个实现

AndroidLogAdapter 与 DiskLogAdapter 分别对应 Logcat 输出与 Disk 保存。

4.1、AndroidLogAdapter 部分代码:

public class AndroidLogAdapter implements LogAdapter {

  @NonNull private final FormatStrategy formatStrategy;

  public AndroidLogAdapter() {
    this.formatStrategy = PrettyFormatStrategy.newBuilder().build();
  }

  public AndroidLogAdapter(@NonNull FormatStrategy formatStrategy) {
    this.formatStrategy = checkNotNull(formatStrategy);
  }

  @Override public boolean isLoggable(int priority, @Nullable String tag) {
    return true;
  }

  @Override public void log(int priority, @Nullable String tag, @NonNull String message) {
    formatStrategy.log(priority, tag, message);
  }

}

很明显只是对是否打印日志进行了控制,其他任务由 PrettyFormatStrategy 来做。

4.2、PrettyFormatStrategy 部分代码:

首先 PrettyFormatStrategy 使用 Builder 设计模式,从前面 AndroidLogAdapter 的构造函数中也可看出来。

  public static class Builder {
    int methodCount = 2;
    int methodOffset = 0;
    boolean showThreadInfo = true;
    @Nullable LogStrategy logStrategy;
    @Nullable String tag = "PRETTY_LOGGER";

    private Builder() {
    }

    @NonNull public Builder methodCount(int val) {
      methodCount = val;
      return this;
    }

    @NonNull public Builder methodOffset(int val) {
      methodOffset = val;
      return this;
    }

    @NonNull public Builder showThreadInfo(boolean val) {
      showThreadInfo = val;
      return this;
    }

    @NonNull public Builder logStrategy(@Nullable LogStrategy val) {
      logStrategy = val;
      return this;
    }

    @NonNull public Builder tag(@Nullable String tag) {
      this.tag = tag;
      return this;
    }

    @NonNull public PrettyFormatStrategy build() {
      if (logStrategy == null) {
        logStrategy = new LogcatLogStrategy();
      }
      return new PrettyFormatStrategy(this);
    }
  }

这样就可以对打印的日志进行配置了,也就是前面进阶使用里面的内容。

下面来看具体打印的实现:

  @Override public void log(int priority, @Nullable String onceOnlyTag, @NonNull String message) {
    checkNotNull(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 void logTopBorder(int logType, @Nullable String tag) {
    logChunk(logType, tag, TOP_BORDER);
  }

  @SuppressWarnings("StringBufferReplaceableByString")
  private void logHeaderContent(int logType, @Nullable String tag, int methodCount) {
    StackTraceElement[] trace = Thread.currentThread().getStackTrace();
    if (showThreadInfo) {
      logChunk(logType, tag, HORIZONTAL_LINE + " Thread: " + Thread.currentThread().getName());
      logDivider(logType, tag);
    }
    String level = "";

    int stackOffset = getStackOffset(trace) + methodOffset;

    //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(HORIZONTAL_LINE)
          .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());
    }
  }

  private void logBottomBorder(int logType, @Nullable String tag) {
    logChunk(logType, tag, BOTTOM_BORDER);
  }

  private void logDivider(int logType, @Nullable String tag) {
    logChunk(logType, tag, MIDDLE_BORDER);
  }

  private void logContent(int logType, @Nullable String tag, @NonNull String chunk) {
    checkNotNull(chunk);

    String[] lines = chunk.split(System.getProperty("line.separator"));
    for (String line : lines) {
      logChunk(logType, tag, HORIZONTAL_LINE + " " + line);
    }
  }

  private void logChunk(int priority, @Nullable String tag, @NonNull String chunk) {
    checkNotNull(chunk);

    logStrategy.log(priority, tag, chunk);
  }

这里就是对日志美化的核心了,但是最终每一行的输出还是不是在此,是由 LogcatLogStrategy 负责的。

4.3、LogcatLogStrategy 部分代码:

LogcatLogStrategy 负责打印每一行日志,当然已经美化过了。

public class LogcatLogStrategy implements LogStrategy {

  static final String DEFAULT_TAG = "NO_TAG";

  @Override public void log(int priority, @Nullable String tag, @NonNull String message) {
    checkNotNull(message);

    if (tag == null) {
      tag = DEFAULT_TAG;
    }

    Log.println(priority, tag, message);
  }
}

这里调用的是系统的 Log 方法逐行打印日志。

5.1、DiskLogAdapter 部分代码:

和 AndroidLogAdapter 类似,DiskLogAdapter 也只是负责控制是否打印日志,具体工作由 CsvFormatStrategy 实现。

public class DiskLogAdapter implements LogAdapter {

  @NonNull private final FormatStrategy formatStrategy;

  public DiskLogAdapter() {
    formatStrategy = CsvFormatStrategy.newBuilder().build();
  }

  public DiskLogAdapter(@NonNull FormatStrategy formatStrategy) {
    this.formatStrategy = checkNotNull(formatStrategy);
  }

  @Override public boolean isLoggable(int priority, @Nullable String tag) {
    return true;
  }

  @Override public void log(int priority, @Nullable String tag, @NonNull String message) {
    formatStrategy.log(priority, tag, message);
  }
}

5.2、CsvFormatStrategy 部分代码:

CsvFormatStrategy 处理后的日志要保存到文件,同样使用 Builder 设计模式:

我们来看下 build() 方法:


@NonNull 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);
}

可以看到文件保存位置为 外置空间根目录 logger 文件夹下。

由 HandlerThread 启动了一个子线程,HandlerThread 实际上还是一个普通的 Thread,不过内部实现了 Looper 循环。

具体实现主要涉及字符串的拼接、格式的调整:

  @Override public void log(int priority, @Nullable String onceOnlyTag, @NonNull String message) {
    checkNotNull(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());
  }

  @Nullable private String formatTag(@Nullable String tag) {
    if (!Utils.isEmpty(tag) && !Utils.equals(this.tag, tag)) {
      return this.tag + "-" + tag;
    }
    return this.tag;
  }

最终还是由 DiskLogStrategy 来负责打印输出日志。

5.3、DiskLogStrategy 部分代码:

这里使用了 Handler 实现线程间的通信:

  @NonNull private final Handler handler;

  public DiskLogStrategy(@NonNull Handler handler) {
    this.handler = checkNotNull(handler);
  }

  @Override public void log(int level, @Nullable String tag, @NonNull String message) {
    checkNotNull(message);

    // do nothing on the calling thread, simply pass the tag/msg to the background thread
    handler.sendMessage(handler.obtainMessage(level, message));
  }

每次有新的日志,调用 handler.sendMessage() 方法。

@SuppressWarnings("checkstyle:emptyblock")
@Override public void handleMessage(@NonNull 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 */ }
    }
    }
}

最后 Handler 接收到消息,就把日志保存到文件中。

Logger 源码分析就到这里了,作者代码写得很优雅,也非常感谢作者向开源社区贡献如此优秀的库。

如果本文对您有所帮助,且您手头还很宽裕,欢迎打赏赞助我,以支付网站服务器和域名费用。 https://paypal.me/wshunli 您的鼓励与支持是我更新的最大动力,我会铭记于心,倾于博客。
本文链接:https://www.wshunli.com/posts/ca2fa1f1.html