Potato icon indicating copy to clipboard operation
Potato copied to clipboard

Logger:Powerful logging library in Android

Open yunshuipiao opened this issue 5 years ago • 0 comments

Logger:Powerful logging library in Android

[TOC]

这篇文章介绍 Android 中常用的日志功能。

日志上在开发中常用的一个工具,可以打印任何需要的信息来辅助开发,系统提供的日志模块在这里不做过多叙述;下面会分别从基本使用和源码去介绍一个强大的日志工具。

基本用法

这个日志的地址和基本使用方法在其开源库主页都有基本介绍:logger.

总结一下对于使用者来说,优点在哪:

  1. 支持打印当前线程和当前方法
  2. 支持 json,xml, list, map, set 等不同的格式输出
  3. 自定义输出到文件,持久化保存日志文件。

使用

这里还有一个常见的开发技巧:对于第三方库来说,尽量自己再包装一层,方便以后替换。

  1. 初始化: ThirdModule.kt

        fun init(context: Context) {
            this.context = context
            initLog()
        }
    
        private fun initLog() {
            Logger.addLogAdapter(AndroidLogAdapter())
            Logger.d("logger init")
        }
    
  2. 新建文件 LogUtils 进行常用方法封装

    object LogUtils {
        const val TAG = "Potato"
        fun d(any: Any) {
            Logger.d(any)
        }
    
        fun i(message: String) {
            Logger.i(message)
        }
        fun w(message: String) {
            Logger.w(message)
        }
        fun e(message: String) {
            Logger.e(message)
        }
        fun wtf(message: String) {
            Logger.wtf(message)
        }
    }
    

上述的基本的用法介绍,对于 debug 级别的日志来说,可以打印很多数据类型,其余级别只支持打印 String 类型的数据。

基本使用和日志输出见官网

进阶使用

在进阶使用中,可以配置相关的日志输出选项,比如线程信息,方法信息等, 见下面代码:


    private fun initLog() {
        val formatStrategy = PrettyFormatStrategy.newBuilder()
            .showThreadInfo(false) // 是否显示线程信息
            .methodCount(0)        // 是否显示方法信息
            .methodOffset(7)       //一个方法会有很多层级调用,偏移的方法数
//            .logStrategy()          //日志输出策略,logcat 还是 disk
            .tag("Potato")        // 自定义 tag
            .build()
        Logger.addLogAdapter(AndroidLogAdapter(formatStrategy))
    }

相应代码的输出如下:

        am_btn_log.setOnClickListener {
            LogUtils.d("LogUtils debug")
            LogUtils.i("LogUtils info")
            LogUtils.w("LogUtils warning")
            LogUtils.e("LogUtils error")
            LogUtils.wtf("LogUtils wtf")

            LogUtils.d(arrayListOf(1, 2, 3))
            LogUtils.d(mapOf(1 to 1, 2 to 2))
            LogUtils.d(setOf(1, 2, 3, 2))

            val json = "{ \"key\": \"content\"}"
            LogUtils.d(json)
        }

image

此外, 还可以控制日志是否打印, 以及自定义 logtag 输出到文件。

        Logger.addLogAdapter(object : AndroidLogAdapter(formatStrategy) {
            override fun isLoggable(priority: Int, tag: String?): Boolean {
                return false
            }
        })
			
			Logger.addLogAdapter(DiskLogAdapter())

优化

在 Android 常见的性能优化中,对于三方库来说,能延迟初始化的一定与延迟初始化,所以这里没有必要在 Provider 中做初始化,可以在具体使用的地方初始化,改动如下:

object LogUtils {
    const val TAG = "Potato"

    init {
        initLog()
        i("LogUtils init done")
    }

    private fun initLog() {
        val formatStrategy = PrettyFormatStrategy.newBuilder()
            .showThreadInfo(false) // 是否显示线程信息
            .methodCount(0)        // 是否显示方法信息
            .methodOffset(7)       //一个方法会有很多层级调用,偏移的方法数
//            .logStrategy()          //日志输出策略,logcat 还是 disk
            .tag("Potato")        // 自定义 tag
            .build()
        Logger.addLogAdapter(AndroidLogAdapter(formatStrategy))
    }

源码分析

image

首先看一下官网的流程图。

初始化

Logger.addLogAdapter(AndroidLogAdapter()): 首先来看初始化工作做了什么:

初始化 AndroidAdapter,如上所示,是接口 LogAdapter的具体实现类,另一个是 DiskLogAdater;分别用来输出到 LogCat 和文件,其内部的接口变量 FormatStrategy 来实现具体的内容。

首先来看 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, 与 CsvFormatStrategy 一样,是 FormatStrategy 的具体实现类,控制不同的输出。

  1. 采用 Build Pattern 创建进阶使用的相关参数

      public static class Builder {
        int methodCount = 2;
        int methodOffset = 0;
        boolean showThreadInfo = true;
        @Nullable LogStrategy logStrategy;
        @Nullable String tag = "PRETTY_LOGGER";
    
  2. 下面是最重要的 log 方法的实现:

      @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)
        // 默认编码 UTF—8, 获取 message 长度
        byte[] bytes = message.getBytes();
        int length = bytes.length;
        // 支持最大长度 4000 个字节Byte, 可使用 adb logcat -d 查看大小
        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 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());
        }
      }
    

    其中一个堆栈帧如下:

    image

    打印的方法数由偏移量决定,上述得到的偏移量为8(默认5 + 非 LoggerPrinter 和 非 Logger), 打印方法数为2, 所以在控制台看到打印的。

    打印流程

    至此,分析了初始化的相关源码, 下面来看一下一个具体的 Logger.d 是如何工作的。

    具体的实现在 Logger 类里面:

      public static void d(@NonNull String message, @Nullable Object... args) {
        printer.d(message, args);
      }
    
      public static void d(@Nullable Object object) {
        printer.d(object);
      }
    
    private static Printer printer = new LoggerPrinter();  // Printer的实现类
    

    当 debug 级别输出一个对象时,

    // LoggerPrinter
      @Override public void d(@Nullable Object object) {
        log(DEBUG, null, Utils.toString(object));
      }
    
      @Override 
    public synchronized void log(int priority,
                                             @Nullable String tag,
                                             @Nullable String message,
                                             @Nullable Throwable 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";
        }
    
      // 按照添加的 LogAdater 进行相应的打印输出(LogCat, Disk)
        for (LogAdapter adapter : logAdapters) {
          if (adapter.isLoggable(priority, tag)) {
            adapter.log(priority, tag, message);
          }
        }
      }
    
    // Utils: debug 下对不同的对象处理成字符串
      public static String toString(Object object) {
        if (object == null) {
          return "null";
        }
        if (!object.getClass().isArray()) {
          return object.toString();
        }
        if (object instanceof boolean[]) {
          return Arrays.toString((boolean[]) object);
        }
        if (object instanceof byte[]) {
          return Arrays.toString((byte[]) object);
        }
        if (object instanceof char[]) {
          return Arrays.toString((char[]) object);
        }
        if (object instanceof short[]) {
          return Arrays.toString((short[]) object);
        }
        if (object instanceof int[]) {
          return Arrays.toString((int[]) object);
        }
        if (object instanceof long[]) {
          return Arrays.toString((long[]) object);
        }
        if (object instanceof float[]) {
          return Arrays.toString((float[]) object);
        }
        if (object instanceof double[]) {
          return Arrays.toString((double[]) object);
        }
        if (object instanceof Object[]) {
          return Arrays.deepToString((Object[]) object);
        }
        return "Couldn't find a correct type for the object";
      }
    

    需要注意的一点:Logger 提供 一次打印 tag, private final ThreadLocal<String> localTag = new ThreadLocal<>(); 是用 ThreadLocal 来保存,隔离线程。

    那么,到现在,Logger.d() 方法输出到控制台的整个流程和源码都完成了分析。

    其余的其他方法, 包括保存在 Disk 的方法这里就不做过多叙述,有疑问可以提 issue 共同探讨。

输出到文件

该日志库除了打印到控制台外,还可以将日志内容输出到文件。使用方法上面介绍过,这里着重说一下如何写到文件。

当进行日志输出时,首先使用 HandlerThread 创建了一个 handler, 在自线程中完成耗时的 IO 操作。

然后 handler 发送消息,并处理消息完成工作。代码如下:

    @SuppressWarnings("checkstyle:emptyblock")
    @Override public void handleMessage(@NonNull Message msg) {
      String content = (String) msg.obj;
			
      // 使用 FileWriter 输出到文件
      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 */ }
        }
      }
    }
 private File getLogFile(@NonNull String folderName, @NonNull String fileName) {
      checkNotNull(folderName);
      checkNotNull(fileName);
			
      File folder = new File(folderName);
      if (!folder.exists()) {
        //TODO: What if folder is not created, what happens then? crash
        folder.mkdirs();
      }

      int newFileCount = 0;
      File newFile;
      File existingFile = null;

      newFile = new File(folder, String.format("%s_%s.csv", fileName, newFileCount));
      while (newFile.exists()) {
        existingFile = newFile;
        newFileCount++;
        newFile = new File(folder, String.format("%s_%s.csv", fileName, newFileCount));
      }

      if (existingFile != null) {
        // log 文件的大小有限制,500 * 1024。 500k。
        if (existingFile.length() >= maxFileSize) {
          return newFile;
        }
        return existingFile;
      }

      return newFile;
    }
  }

具体写入的代码如下:

private void writeLog(@NonNull FileWriter fileWriter, @NonNull String content) throws IOException {
  checkNotNull(fileWriter);
  checkNotNull(content);

  fileWriter.append(content);
}

总结

  1. Logger 中面向接口编程实践的很好,运用了不同的设计模式。
  2. 主流程(如上图)设计简单,易于理解。

yunshuipiao avatar May 14 '19 09:05 yunshuipiao