Flutter | 日志还能这么打印,太秀了!

10,599

写程序出 Bug 是不可避免的事情,没有哪一个人的逻辑在每时每刻都是正确无误的。很多时候,改 Bug 的时间比写代码的时间还长。你偶尔也会怀疑到底自己是在写 Bug 还是写程序~

我甚至认为,程序员排查 Bug 的能力在某种程度上决定了他的技术水平。通常我们会从控制台打印出的日志找出程序崩溃的具体位置,然后断点调试,一步一步找到元凶。本文将教读者使用 logger 这个日志打印库,极大的加快你排查问题的速度。

如果你在 Flutter 中仍在使用 print(),debugPrint() 打印日志,我觉得是时候了解 logger 这个日志组件了,因为它真的很优雅

logger 简单介绍

logger 是一个纯 dart 语言编写的日志打印库,不依赖于特定平台。它非常轻巧且可扩展非常强,打印出来的日志特别的漂亮,它完美的实现了堆栈信息的自定义打印,多样的打印器、过滤器。你可以将日志打印到控制台,输出到文件,临时保存到内存中等。

我使用这个日志打印组件已经有一段时间了,整体感觉非常的稳定,我特别喜欢它可以打印出方法的堆栈信息,其次作为一个有些许颜控的人,它打印出的日志格式和颜色我都非常喜欢。这个组件的作者是居住在德国慕尼黑的一个小伙,他说这个组件的灵感来源于 Android 平台的日志组件 logger

logger 的基本使用和封装

首先来看看 logger 项目的整体代码结构,由三大部分组成。层次非常的清晰,作者将类的继承和对象的组合发挥到了极致,类名让人一眼看上去就知道是什么意思,每个类都做到了职责单一,将业务抽象成了代码,可见作者的代码水平非常的高。filters 目录中的是过滤器,outputs 输出器指定日志输出位置,printers 打印器规定了日志打印的样式和堆栈信息等

logger 代码结构

基本使用

logger 的使用非常的简单,在 pubspec.yaml 中添加如下依赖。

logger: ^1.0.0

它的日志级别分为7个,如下所示。默认的日志级别是 verbose,即会打印出所有 >= verbose 级别的日志。

enum Level {
  verbose,
  debug,
  info,
  warning,
  error,
  wtf,
  nothing,
}

当你要打印日志的时候,只需实例化一个 Logger 对象,然后调用不同级别的打印方法就可以了。

void main() {
  var _logger = Logger(
    printer: PrettyPrinter(
      methodCount: 0,
    ),
  );
  _logger.v('verbose message');
  _logger.d('debug message');
  _logger.i('info message');
  _logger.w('warning message');
  _logger.e('error message');
  _logger.wtf('wft message');
}

下图是上面代码所打印出来的效果。

logger 除了使用简单之外,输出的日志也很优美。在 Logger 的构造函数中,我们可以传入特定的打印器、过滤器、输出位置等参数自由配置,下面是 Logger 的构造函数。

Logger({
    LogFilter? filter,  // 过滤器,可以区分开发环境与生产环境
    LogPrinter? printer,  // 打印器,控制日志输出样式和堆栈信息等
    LogOutput? output,  // 输出器,控制日志输出位置。可以是控制台、文件、内存
    Level? level,
  })  : _filter = filter ?? DevelopmentFilter(),
        _printer = printer ?? PrettyPrinter(),
        _output = output ?? ConsoleOutput() {
    _filter.init();
    _filter.level = level ?? Logger.level;
    _printer.init();
    _output.init();
  }

如果不传入任何参数,默认过滤器是开发者模式,打印器是漂亮的打印器、输出位置是控制台。

简单封装

打印日志在项目中是全局的,为了能在项目中任意地方使用打印功能,最好封装一下,如下是一个简单的封装,Logger 只需要实例化一次,之后在项目中任何地方都可以调用各个级别的打印方法。这里我使用了 PrefixPrinter 打印器包装了 PrettyPrinter 打印器。

class Log {
  static Logger _logger = Logger(
    printer: PrefixPrinter(PrettyPrinter()),
  );
  
  static void v(dynamic message) {
    _logger.v(message);
  }

  static void d(dynamic message) {
    _logger.d(message);
  }

  static void i(dynamic message) {
    _logger.i(message);
  }

  static void w(dynamic message) {
    _logger.w(message);
  }

  static void e(dynamic message) {
    _logger.e(message);
  }

  static void wtf(dynamic message) {
    _logger.wtf(message);
  }
}

logger 的打印器

logger 的打印器是 logger 目前最核心的功能,本文会重点讲解打印器。以 PrettyPrinter() 打印器为例,首先看一下它的构造函数,如下。

PrettyPrinter({
    this.stackTraceBeginIndex = 0,  // 方法栈的开始下标
    this.methodCount = 2,  // 打印方法栈的个数
    this.errorMethodCount = 8, // 自己传入方法栈对象后该参数有效
    this.lineLength = 120,  // 每行最多打印的字符个数
    this.colors = true,  // 日志是否有颜色
    this.printEmojis = true,// 是否打印 emoji 表情
    this.printTime = false,  // 是否打印时间
  })

使用 PrettyPrinter 不指定任何参数,默认的打印方式如上,接着用我们上面刚刚封装好的代码。打印看看效果。

// LogTest.dart
void main(){
  Log.w("PrettyPrinter warning message");
}  

LogTest.dart 的 main 方法中打印了一条 warning 级别的日志,因为没有指定 PrettyPrinter 的任何参数,所以打印出的栈方法默认是#0#1两条。读者应该知道调用方法其实是不停的在向系统压栈,最后调用的方法肯定在栈顶,很显然,#0是栈顶。那么栈底调用的方法是哪个呢?其实读者只要指定打印的方法栈个数足够大,就可以看到了

不知道读者有没有发现一个问题,我们封装后,每次打印的日志都会携带一条 #0 的方法栈日志。大多时候我们不关心封装里面的方法调用,只关心这条日志是从哪打印的(上面是#1),这样就可以快速定位到对应代码的位置。

现在,思考如何将#0去除?其实也很简单,通过查看源码。我们只要指定 stackTraceBeginIndex 和 methodCount 的值,就可以控制输出了。现在为 PrettyPrinter 指定这两个参数的值,分别是 5 和 1。

static Logger _logger = Logger(
    printer: PrefixPrinter(PrettyPrinter(
      stackTraceBeginIndex: 5,
      methodCount: 1
    )),
  );

为什么 stackTraceBeginIndex 的值是 5 呢。读者可以查看 PrettyPrinter 类中 formatStackTrace 方法,断点调试查看方法栈信息即可得到具体的值

之后再次打印日志,就只有刚才的#1栈方法会被打印了。

logger 的过滤器

logger 目前有两种过滤器 DevelopmentFilter 和 ProductionFilter。使用 DevelopmentFilter 在 debug mode 时日志都会被打印。如果你将 APK 打 Release 包时,所有日志都将不会打印。

而使用 ProductionFilter,无论是 debug mode 还是 将 APK 打 Release 包放入生产环境,日志都将会打印。

logger 是如何实现这种功能的呢?通过查看其源码,也非常的简单巧妙。下面是 DevelopmentFilter 的实现,由于 assert 断言语句只有在 debug mode 时才会被调用,所以 shouldLog = true,日志就可以打印了。在生产环境 assert 断言语句是不执行的,这样就屏蔽了所有日志的输出。

class DevelopmentFilter extends LogFilter {
  @override
  bool shouldLog(LogEvent event) {
    var shouldLog = false;
    assert(() {  // assert 断言只有在 debug mode 才会被调用。
      if (event.level.index >= level!.index) {
        shouldLog = true;
      }
      return true;
    }());
    return shouldLog;
  }
}

logger 的输出器

logger 充分考虑到了用户的使用场景,支持日志打印在控制台、文件、内存。甚至可以使用 MultiOutput 输出器将日志同时输出在多个位置。这里就不详细讲解这些 API 的使用方法,读者可以自行尝试。

彩色日志的实现原理

这个项目最吸引我的一个点,就是它打印出来的日志真的很好看!颜色分明,看上去特别的舒服。不知道你是否也好奇控制台是如何输出这些彩色日志的?

这必须谈到 ANSI 转义序列,通过它就可以控制文本在终端上的光标位置、颜色和其他选项。一个标准的 ANSI 转义序列以 ASCII 码值 31 加上一个左方括号组成。因为 31 的 16 进制表示是 x1B,所以转义之后最终就是这样子:\x1B[。左方括号[后面就可以指定具体的输出模式了,比如你想让helloworld这个单词输出颜色为红色,那么整个字符串序列就是这样的。其中31m指定输出到控制台的颜色为红色。

"\x1B[31m helloworld"

关于 ANSI 转义序列的更多输出模式和使用方法,读者可以查阅相关资料进一步了解。在 logger 组件中,AnsiColor 这个类实现了让不同级别的日志呈现出不同颜色的效果。

写在最后

本文介绍了 logger 日志组件的详细使用方法。向读者介绍了 logger 的打印器、过滤器、输出器。对其参数和可能出现疑惑的地方进行了详细的说明。并在最后揭开了如何打印彩色日志的原理。读者看完全文,应该有一种豁然开朗的感觉,其实一个日志组件的基本组成就是这样。由于 logger 组件的可扩展性非常的强,我们完全可以通过继承 logger 的基类来实现自己的打印器、过滤器和输出器。

如果你对我感兴趣,请移步到 blogss.cn , 或关注公众号:程序员小北,进一步了解。

  • 如果本文帮助到了你,欢迎点赞和关注,这是我持续创作的动力 ❤️
  • 由于作者水平有限,文中如果有错误,欢迎在评论区指正 ✔️
  • 本文首发于掘金,未经许可禁止转载 ©️