04 | Android 高级进阶(源码剖析篇) 优美的日志框架 logger

作者简介:ASCE1885, 《Android 高级进阶》作者。
本文由于潜在的商业目的,未经授权不开放全文转载许可,谢谢!
本文分析的源码版本已经 fork 到我的 Github

夜空中的你

logger 可以说是 Android 平台最著名的日志框架,从一出现就吸引了广大开发者的关注。演化至今,logger 框架具备的能力主要有:

  • 以类似表格的方式展示日志,视觉优美,能够清晰的分隔不同的日志记录
  • 全面的信息展示,支持线程信息,函数调用栈信息的展示
  • 支持点击跳转到记录日志源码的位置(Android Studio 提供的能力)
  • 支持 JSON,XML,List,Map 和 Set 的格式化输出
  • 默认支持 Logcat 和文件两种日志记录输出方式,并提供动态可扩展的能力

logger 日志记录在 Logcat 中的效果和说明如下图所示:

在最初的几个版本中,logger 框架只有两个类:Logger 和 LogLevel,前者是日志记录的核心实现,后者是日志级别定义。功能上只支持打印日志到 Logcat 中,不支持记录日志到文件,架构设计上也比较简单,扩展性不强。随着多个版本的迭代,logger 框架以面向接口编程的方式优化了整体的架构设计,可扩展性更强。本文写作时 2.1.1 版本的类结构图如下所示:

MacHi 2017-10-26 17-00-05.png

可以看到,整体还是很清晰的,有四个接口:

  • LogAdapter:日志的整体输出方式的适配器
  • FormatStrategy:日志输出格式的策略定义
  • LogStrategy:日志如何输出的策略定义
  • Printer:对框架使用者而言的方法定义

而 Logger 类是作为对外的核心类存在的。

架构设计

logger 的整体架构和生命周期官网已经给出来,如下图所示,可以看到,有五层的调用关系:

how_it_works.png

其中 Logger 是对外的接口类,开发者调用这个类提供的方法来将需要记录的日志信息传递给 logger 框架,而 Logger 会将调用委托给 Printer 接口的实现类 LoggerPrinter,实际的日志记录行为都是 LoggerPrinter 负责的。LoggerPrinter 提供了一个方法可以添加 LogAdapter 的实例列表,从图中我们也可以看出来,这样开发者只需调用一个方法就可以同时实现多种类型的日志记录方式。LogAdapter 正是为了实现多种方式日志记录而实现的适配器接口,logger 默认提供了 AndroidLogAdapterDiskLogAdapter 这两种适配器,分别用来实现 Logcat 日志记录和文件日志记录。同时日志记录会有不同的格式需求,这是通过 FormatStrategy 接口实现的,logger 中默认实现了两种格式策略,一种是 PrettyFormatStrategy,另一种是 CsvFormatStrategy,分别适用于 Logcat 日志记录和文件日志记录,主要是起到美观和易读的目的。最后 FormatStrategy 实现类会再调用一个日志记录策略类 LogStrategy 来决定是把日志打印到 Logcat 还是记录到文件中。

实现细节

下面我们就按照接口调用层级从最底层依次往上剖析,首先来看下 LogStrategy 接口及其实现类。

LogStrategy

LogStrategy 接口定义了日志如何输出的策略,从名字可以看出,是应用了策略模式,它的用意是针对一组算法,将每个算法封装起来,让它们实现一个共同的接口,使它们可以相互替换。策略模式可以实现算法在不影响使用者的情况下发生变化。试想一下,如果不使用策略模式,那么我们要实现运行时灵活的根据具体条件切换具体的算法,那么是不是只能通过 if...else 等类似方式来实现?也就是把所有具体算法的选择都封装在了一个类中,在需要新增或者删除某个算法时,都需要到这个类中进行修改。而策略模式,则屏蔽了内部的修改,一切由使用者来选择,符合开放-封闭原则

由于 FormatStrategy 也是使用的策略模式,同时它的实现类中实例化了 LogStrategy 的实现类,因此类结构图结合在一起看:

LogStrategy 接口的定义很简单,只有 log 这个方法:

public interface LogStrategy {

  /**
   * log 方法
   * @param priority 优先级
   * @param tag 标签
   * @param message 日志信息
   */
  void log(int priority, String tag, String message);
}

然后由具体的策略类实现这个 log 方法,在其中实现各自的日志记录算法,具体到 LogcatLogStrategy 类,它的目的是实现将日志打印到 Android Logcat 中,因此它的算法实现很简单,直接调用 Android Log 的 println 方法即可,如下所示:

public class LogcatLogStrategy implements LogStrategy {

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

}

而另一个子类 DiskLogStrategy 实现则稍微复杂一点,因为涉及到要将日志信息写入文件,所以需要在子线程中实现文件写入操作,具体是使用 Handler 来处理,如下所示:

public class DiskLogStrategy implements LogStrategy {

  private final Handler handler;

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

  @Override public void log(int level, String tag, String message) {
    // 因为我们不能控制调用者是在主线程还是子线程,因此这里直接将消息抛到 handler 中处理(毕竟是写文件)
    handler.sendMessage(handler.obtainMessage(level, message));
  }
}

可以看到, Handler 以构造函数依赖注入的方式传给 DiskLogStrategy,保证良好的扩展性,同时给出了一个默认的 Handler 实现 WriteHandler,它以 DiskLogStrategy 的静态内部类的形式存在。WriteHandler 是 Handler 的子类,熟悉 Handler 的我们肯定知道它的核心处理逻辑应该在 handleMessage 方法中。核心代码如下所示:

@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 */ }
        }
    }
}

可以看到,日志信息写文件很简单,可以分为两步:

  • 第一步,创建日志文件,这一步在 getLogFile 方法中实现,我们直接上代码,相关注释也很详细,这里需要注意,我们的日志文件后缀名是 .csv,这个后面会进一步介绍。
/**
 * 每记录一条日志都会调用一次,因此一般建议在测试环境中使用,线上版本应该关闭文件日志记录
 * @param folderName 日志文件目录名
 * @param fileName 日志文件名
 * @return
 */
private File getLogFile(String folderName, String fileName) {

    // 如果文件夹不存在,则先创建
    File folder = new File(folderName);
    if (!folder.exists()) {
        folder.mkdirs();
    }

    // 每个日志文件有大小限制,由使用者来指定,每一条新的日志信息都会记录到
    // 当前还没超出大小限制的最新一个文件中,newFileCount 用来递增文件名
    int newFileCount = 0;
    File newFile;
    File existingFile = null;

    // 遍历日志目录中已经存在的日志文件,找到最新的那个文件并放在 existingFile 变量中
    newFile = new File(folder, String.format("%s_%s.csv", fileName, newFileCount));
    while (newFile.exists()) {
        existingFile = newFile;
        newFileCount++;
        // 创建一个新的日志文件,需要注意这只是在内存中创建 File 文件映射对象,此刻并不会在硬盘中创建实际文件
        // 除非你往这个文件中写入数据,或者调用 newFile.createNewFile() 创建真实文件
        newFile = new File(folder, String.format("%s_%s.csv", fileName, newFileCount));
      }

    if (existingFile != null) {
        // 如果最新的日志文件 existingFile 超出大小限制,则使用新创建的 newFile 文件
        if (existingFile.length() >= maxFileSize) {
            return newFile;
        }
        return existingFile;
    }

    return newFile;
}
  • 第二步,使用 FileWriter 将日志信息写入文件,FileWriter 类从 OutputStreamReader 类继承而来,主要实现按字符向流中写入数据,我们的日志信息是文本形式存在,因此选择 FileWriter 是理所当然的。具体用法可以参见 Java SE 相关图书,从上面代码也可以看出它的基本用法。

FormatStrategy

通过前面的介绍,我们对 FormatStrategy 已经不陌生,它也是使用的策略模式,定义了日志输出格式的策略,接口代码如下所示,和 LogStrategy 定义其实是一样的,只不过两者目的不同。

public interface FormatStrategy {
  void log(int priority, String tag, String message);
}

FormatStrategy 的两个子类 CsvFormatStrategy 和 PrettyFormatStrategy 分别实现 csv 格式输出和 的本文开头所看到的那种漂亮的类表格化格式输出算法。

CsvFormatStrategy

首先,为了让不熟悉 csv 的读者有个基本的了解,我们先介绍下 csv 的基本知识。csv 是一种文件格式,全称是 Comma-Separated Values,也就是逗号分隔值文件,它是一种文本文件,它的文件格式有如下限定:

  • 每条记录占一行
  • 以逗号为分隔符
  • 逗号前后的空格会被忽略
  • 字段中包含有逗号,那么该字段必须用双引号括起来
  • ......

更详细的介绍可以参见《The Comma Separated Value (CSV) File Format》。一个 csv 文件内容示例如下所示:

John,Doe,120 jefferson st.,Riverside, NJ, 08075
Jack,McGinnis,220 hobo Av.,Phila, PA,09119
"John ""Da Man""",Repici,120 Jefferson St.,Riverside, NJ,08075
Stephen,Tyler,"7452 Terrace ""At the Plaza"" road",SomeTown,SD, 91234
,Blankman,,SomeTown, SD, 00298
"Joan ""the bone"", Anne",Jet,"9th, at Terrace plc",Desert City,CO,00123

具体到 CsvFormatStrategy,它的 csv 文件格式中每条记录的组成如下:

epoch 时间戳(机器可读的), ISO8601 时间戳 (人类可读的), 日志级别, 日志标签, 日志信息

CsvFormatStrategy 类的可选入参有四个,分别是:

private final Date date; // 日期
private final SimpleDateFormat dateFormat; // 日期的格式
private final LogStrategy logStrategy; // LogStrategy 实例
private final String tag; // 日志标签

算是比较多的,为了减少对象创建过程中引入多个重载的构造函数,或者避免 setters 方法的过度使用,因此使用 Builder 模式来进行实例的初始化,如果读者对该模式还不熟悉,可以阅读《Android 高级进阶》一书的《Builder 模式详解》一节。这些参数都是可选的,如果调用者没有传入的话,会有默认值,这是在 Builder 类的 build 方法中实现的:

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

推荐阅读更多精彩内容