寫程式出 Bug 是不可避免的事情,沒有哪一個人的邏輯在每時每刻都是正確無誤的。很多時候,改 Bug 的時間比寫程式碼的時間還長。你偶爾也會懷疑到底自己是在寫 Bug 還是寫程式~
我甚至認為,程式設計師排查 Bug 的能力在某種程度上決定了他的技術水平。通常我們會從控制檯列印出的日誌找出程式崩潰的具體位置,然後斷點除錯,一步一步找到元凶。本文將教讀者使用 logger 這個日誌列印庫,極大的加快你排查問題的速度。
如果你在 Flutter 中仍在使用 print(),debugPrint() 列印日誌,我覺得是時候瞭解 logger 這個日誌元件了,因為它真的很優雅。
logger 簡單介紹
logger 是一個純 dart 語言編寫的日誌列印庫,不依賴於特定平臺。它非常輕巧且可擴充套件非常強,列印出來的日誌特別的漂亮,它完美的實現了堆疊資訊的自定義列印,多樣的列印器、過濾器。你可以將日誌列印到控制檯,輸出到檔案,臨時儲存到記憶體中等。
我使用這個日誌列印元件已經有一段時間了,整體感覺非常的穩定,我特別喜歡它可以列印出方法的堆疊資訊,其次作為一個有些許顏控的人,它列印出的日誌格式和顏色我都非常喜歡。這個元件的作者是居住在德國慕尼黑的一個小夥,他說這個元件的靈感來源於 Android 平臺的日誌元件 logger。
logger 的基本使用和封裝
首先來看看 logger 專案的整體程式碼結構,由三大部分組成。層次非常的清晰,作者將類的繼承和物件的組合發揮到了極致,類名讓人一眼看上去就知道是什麼意思,每個類都做到了職責單一,將業務抽象成了程式碼,可見作者的程式碼水平非常的高。filters 目錄中的是過濾器,outputs 輸出器指定日誌輸出位置,printers 列印器規定了日誌列印的樣式和堆疊資訊等。
基本使用
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 , 或關注公眾號:程式設計師小北,進一步瞭解。
- 如果本文幫助到了你,歡迎點贊和關注,這是我持續創作的動力 ❤️
- 由於作者水平有限,文中如果有錯誤,歡迎在評論區指正 ✔️
- 本文首發於掘金,未經許可禁止轉載 ©️