Logger源码理解分析

安卓开发过程中,log日志是我们接触最多的一部分。如何优雅的获取log日志呢?我个人推荐使用Logger

GitHub/Logger传送门

Logger效果展示

备注:

log级别 颜色
Verbose BBBBBB
Debug 0070BB
Info 48BB31
Warm BBBB23
Error FF0006
Assert 8F0005

控制台日志

代码部分:
Logcat代码部分.png
截图部分:
Verbose.png
Debug.png
Info.png
Warm.png
Error.png
Assert.png
json.png

CsvFile文件日志

代码部分
CsvFile代码部分.png
截图部分(文件保存在手机存储logger目录下)
CsvFile.png

源码分析

这些是 Logger 最基础的用法,同时还支持 xml 的打印。而且在GitHub上的README中有自定义参数的用法:

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

不过,我们先从就从 v 方法开始分析:

Logger.java 部分代码
     private static Printer printer = new LoggerPrinter();

  public static void v(String message, Object... args) {
        printer.v(message, args);
  }
  • 首先,我们进入 Logger 类中,看到 v() 方法调用了 printer.v()

  • Printer是接口,实现类是 LoggerPrinter,下一步进入 LoggerPrinter 类查看 v 方法的细节

LoggerPrinter.java 部分代码
    private final ThreadLocal<String> localTag = new ThreadLocal<>();
    
    @Override 
    public Printer t(String tag) {
        if (tag != null) {
          localTag.set(tag);
        }
        return this;
    }
    
    @Override 
    public void v(String message, Object... args) {
        log(VERBOSE, null, message, args);
    }

    private synchronized void log(int priority, Throwable throwable, String msg, Object... args) {
        String tag = getTag();
        String message = createMessage(msg, args);
        log(priority, tag, message, throwable);
    }
    
    private String getTag() {
        String tag = localTag.get();
        if (tag != null) {
          localTag.remove();
          return tag;
        }
        return null;
    }
    
    @Override 
    public synchronized void log(int priority, String tag, String message, 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);
          }
        }
    }
  • 从上面的代码可以看出 LoggerPrinter 类中有一个ThreadLocal 用于存放标签 (给线程设置局部变量,避免出现线程并发问题,在Handler源码理解分析文末有简单的介绍),同时可以发现有 t() 方法用于值的注入并且返回 Printer 对象,这说明可以链式调用

  • log() 方法中取出做为 tag,不过细心的可以看出在 getTag() 的时候虽然取出了 tag,但是明显取出 tag 之后就将其置空,说明tag只能使用一次

  • 注意:t()方法只是给当前的线程设置一个仅能使用一次的标签参数

      Logger.t("lalala").d("测试t()方法1");
      Logger.d("测试t()方法2");
    
t方法使用.png
  • 从上图可以看出 t() 方法可可设置 tag 标签,不过是在默认 "PRETTY_KIGGER" 之后拼上添加的 tag (细节请往下看PrettyFormatStrategy类的formatTag()方法)

  • 回到 LoggerPrinterv() 方法,它最终调用了 Printer 接口的 log() 方法,所以可以看出无论是 v,i,d,w 等方法最终都是调用 log()。当中要特别注意的是 for (LogAdapter adapter : logAdapters) 这说明了它可以配置多个 Adapter

  • LoggerPrinter最终调用了接口 LogAdapterlog() 方法,所以我们要找 LogAdapter 的实现类。不过在调用之前通过 isLoggable(priority, tag) 对输出进行过滤,我们可以在初始化的时候通过重写这个方法来定义自己的规则

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

我们在README中可以看到两个实现类:

AndroidLogAdapterDiskLogAdapter

下面我们分别对它们进行分析

AndroidLogAdapter.java代码

public class AndroidLogAdapter implements LogAdapter {

  private final FormatStrategy formatStrategy;

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

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

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

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

}
  • 可以看出 AndroidLogAdapter 有两个构造方法,从无参的构造方法可以看出 PrettyFormatStrategy 使用的是 Builder设计模式。明显,无参的是我们在篇首进行测试使用的,另一个是用于自定义属性参数。

  • 我们再进一步观察 PrettyFormatStrategy 类中发生了什么

PrettyFormatStrategy.java部分代码
  private final int methodCount;
  private final int methodOffset;
  private final boolean showThreadInfo;
  private final LogStrategy logStrategy;
  private final String tag;
    
@Override public void log(int priority, String onceOnlyTag, String 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 String formatTag(String tag) {
    if (!Utils.isEmpty(tag) && !Utils.equals(this.tag, tag)) {
      return this.tag + "-" + tag;
    }
    return this.tag;
}

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

private void logChunk(int priority, String tag, String chunk) {
    logStrategy.log(priority, tag, chunk);
}
  • log() 方法中开始通过 formatTag() 拼接添加的 tag 标签
  • 通过 logTopBorder() 打印输出的头部边界
  • 通过 logHeaderContent() 打印输出线程信息,以及调用该方法的所在代码位置
  • logContent() 之前先进行 message 超长处理,之后在 logContent() 中进行换行格式处理
  • System.getProperty("line.separator")//换行符,功能和"\n"是一致的,但是此种写法避免了 Windows和Linux的冲突
  • 最后调用接口 LogStrategylog() 方法进行打印,所以我们要去寻找它的实现类 LogcatLogStrategy
  • 明显,我们可以自定义打印的策略,通过Builder传入,否则将使用默认的 LogcatLogStrategy
LogcatLogStrategy.java
public class LogcatLogStrategy implements LogStrategy {

  @Override public void log(int priority, String tag, String message) {
    Log.println(priority, tag, message);
  }

}
  • 看到这里调用了系统自带的 Log 来打印

DiskLogAdapter.java部分代码

public class DiskLogAdapter implements LogAdapter {

  private final FormatStrategy formatStrategy;

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

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

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

  @Override public void log(int priority, String tag, String message) {
    formatStrategy.log(priority, tag, message);
  }
}
  • AndroidLogAdapter 一样我们直接分析分析 CsvFormatStrategy
CsvFormatStrategy.java部分代码
    private final Date date;
  private final SimpleDateFormat dateFormat;
  private final LogStrategy logStrategy;
  private final String tag;

  private CsvFormatStrategy(Builder builder) {
    date = builder.date;
    dateFormat = builder.dateFormat;
    logStrategy = builder.logStrategy;
    tag = builder.tag;
  }

  public static Builder newBuilder() {
    return new Builder();
  }

  @Override public void log(int priority, String onceOnlyTag, String 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());
  }
  
  public static final class Builder {
    .
    .
    .
    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);
    }
    
  }
  • 有了 PrettyFormatStrategy 的分析,相比较这个反而会更简单一点
  • 主要是对字符串的拼接,格式的调整
  • 所有重点就落到了 LogSrategy 的实现类了,在 Builder 中的builder()方法中问题最大的应该是 HandlerThreadDiskLogStrategy
  • HandlerThread 实际上还是一个普通的Thread,不过内部实现了 Looper 循环。好处: 在子线程中实现Looper,减轻了UI线程looper的压力。如有问题可以结合Handler源码理解分析进行理解
  • 并且可以看出日志文件保存在 Environment.getExternalStorageDirectory().getAbsolutePath() + "logger" 文件夹下
  • 接下来我们开始分析 DiskLogStrategy
DiskLogStrategy.java部分代码
private final Handler handler;

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

 @Override public void log(int level, String tag, String message) {
 // do nothing on the calling thread, simply pass the tag/msg to the background thread
 handler.sendMessage(handler.obtainMessage(level, message));
}


static class WriteHandler extends Handler {

private final String folder;
private final int maxFileSize;

WriteHandler(Looper looper, String folder, int maxFileSize) {
  super(looper);
  this.folder = folder;
  this.maxFileSize = maxFileSize;
}

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

/**
 * This is always called on a single background thread.
 * Implementing classes must ONLY write to the fileWriter and nothing more.
 * The abstract class takes care of everything else including close the stream and catching IOException
 *
 * @param fileWriter an instance of FileWriter already initialised to the correct file
 */
private void writeLog(FileWriter fileWriter, String content) throws IOException {
  fileWriter.append(content);
}

private File getLogFile(String folderName, String fileName) {
  .
  .
  .
  return newFile;
}
  • 从上面的代码可以看出 DiskLogStrategy 类中有个静态内部类继承自 Handler, Looper 却是从 HandlerThread 中得到的,说明handleMessage将会在一个子线程中执行
  • 通过 handleMessage 则是将 log() 方法发送的 msg 中的内容写入文件
  • 个人理解:使用这种模式是为了保证日志的有序性避免多个线程对同一个文件进行编辑,且在子线程中保证不阻塞UI线程

How it works

how_it_works.png

在GitHub上,官方给出了原理图,我们分析的方向也大致如此。注意:Printer与LogAdapter的关系为1对多

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,402评论 6 499
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,377评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,483评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,165评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,176评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,146评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,032评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,896评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,311评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,536评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,696评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,413评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,008评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,815评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,698评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,592评论 2 353

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,072评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,651评论 18 139
  • 想一步步地抽离生活的泥沼,好不容易快出来了,一不小心,却陷得更深了
    龚思源阅读 162评论 0 0
  • 阿里推客新推出不到3个月,在浏览器插件和群发软件推出的情况下,就马不停蹄的进入到了APP的研发阶段,在经过不断地努...
    乱流年中阅读 1,573评论 0 0
  • 正则表达式 纯文本做限制处理.可以用来检查一个字符串是否包含某种子串,将匹配的子串做替换或者从某个串中取出符合某个...
    BigBossZhu阅读 498评论 0 2