Flutter | 日誌還能這麼列印,太秀了!

Thatcher_Li發表於2021-05-17

寫程式出 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 , 或關注公眾號:程式設計師小北,進一步瞭解。

  • 如果本文幫助到了你,歡迎點贊和關注,這是我持續創作的動力 ❤️
  • 由於作者水平有限,文中如果有錯誤,歡迎在評論區指正 ✔️
  • 本文首發於掘金,未經許可禁止轉載 ©️

相關文章