flutter11 - 調試程序
本文轉載自:Flutter實戰 (杜文 編著)
Dart 分析器
在運行應用程序前,請運行flutter analyze
測試你的代碼。這個工具是一個靜態代碼檢查工具,它是dartanalyzer
工具的一個包裝,主要用於分析代碼並幫助開發者發現可能的錯誤,比如,Dart分析器大量使用了代碼中的類型註釋來幫助追蹤問題,避免var
、無類型的參數、無類型的列表文字等。
如果你使用IntelliJ的Flutter插件,那麼分析器在打開IDE時就已經自動啓用了,如果讀者使用的是其它IDE,強烈建議讀者啓用Dart 分析器,因爲在大多數時候,Dart 分析器可以在代碼運行前發現大多數問題。
Dart Observatory (語句級的單步調試和分析器)
如果我們使用flutter run
啓動應用程序,那麼當它運行時,我們可以打開Observatory工具的Web頁面,例如Observatory默認監聽http://127.0.0.1:8100/,可以在瀏覽器中直接打開該鏈接。直接使用語句級單步調試器連接到您的應用程序。如果您使用的是IntelliJ,則還可以使用其內置的調試器來調試您的應用程序。
Observatory 同時支持分析、檢查堆等。有關Observatory的更多信息請參考Observatory 文檔。
如果您使用Observatory進行分析,請確保通過--profile
選項來運行flutter run
命令來運行應用程序。 否則,配置文件中將出現的主要問題將是調試斷言,以驗證框架的各種不變量(請參閱下面的“調試模式斷言”)。
debugger()
聲明
當使用Dart Observatory(或另一個Dart調試器,例如IntelliJ IDE中的調試器)時,可以使用該debugger()
語句插入編程式斷點。要使用這個,你必須添加import 'dart:developer';
到相關文件頂部。
debugger()
語句採用一個可選when
參數,您可以指定該參數僅在特定條件爲真時中斷,如下所示:
void someFunction(double offset) {
debugger(when: offset > 30.0);
// ...
}
print
、debugPrint
、flutter logs
Dart print()
功能將輸出到系統控制檯,您可以使用flutter logs
來查看它(基本上是一個包裝adb logcat
)。
如果你一次輸出太多,那麼Android有時會丟棄一些日誌行。爲了避免這種情況,您可以使用Flutter的foundation
庫中的debugPrint()
。 這是一個封裝print,它將輸出限制在一個級別,避免被Android內核丟棄。
Flutter框架中的許多類都有toString
實現。按照慣例,這些輸出通常包括對象的runtimeType
單行輸出,通常在表單中ClassName(more information about this instance…)。 樹中使用的一些類也具有toStringDeep
,從該點返回整個子樹的多行描述。已一些具有詳細信息toString
的類會實現一個toStringShort
,它只返回對象的類型或其他非常簡短的(一個或兩個單詞)描述。
調試模式斷言
在Flutter應用調試過程中,Dart assert
語句被啓用,並且Flutter框架使用它來執行許多運行時檢查來驗證是否違反一些不可變的規則。
當一個不可變的規則被違反時,它被報告給控制檯,並帶有一些上下文信息來幫助追蹤問題的根源。
要關閉調試模式並使用發佈模式,請使用flutter run --release
運行您的應用程序。 這也關閉了Observatory調試器。一箇中間模式可以關閉除Observatory之外所有調試輔助工具的,稱爲“profile mode”,用--profile
替代--release
即可。
調試應用程序層
Flutter框架的每一層都提供了將其當前狀態或事件轉儲(dump)到控制檯(使用debugPrint
)的功能。
Widget 樹
要轉儲Widgets樹的狀態,請調用debugDumpApp()
。 只要應用程序已經構建了至少一次(即在調用build()
之後的任何時間),您可以在應用程序未處於構建階段(即,不在build()
方法內調用 )的任何時間調用此方法(在調用runApp()
之後)。
如, 這個應用程序:
import 'package:flutter/material.dart';
void main() {
runApp(
new MaterialApp(
home: new AppHome(),
),
);
}
class AppHome extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Material(
child: new Center(
child: new FlatButton(
onPressed: () {
debugDumpApp();
},
child: new Text('Dump App'),
),
),
);
}
}
…會輸出這樣的內容(精確的細節會根據框架的版本、設備的大小等等而變化):
I/flutter ( 6559): WidgetsFlutterBinding - CHECKED MODE
I/flutter ( 6559): RenderObjectToWidgetAdapter<RenderBox>([GlobalObjectKey RenderView(497039273)]; renderObject: RenderView)
I/flutter ( 6559): └MaterialApp(state: _MaterialAppState(1009803148))
I/flutter ( 6559): └ScrollConfiguration()
I/flutter ( 6559): └AnimatedTheme(duration: 200ms; state: _AnimatedThemeState(543295893; ticker inactive; ThemeDataTween(ThemeData(Brightness.light Color(0xff2196f3) etc...) → null)))
I/flutter ( 6559): └Theme(ThemeData(Brightness.light Color(0xff2196f3) etc...))
I/flutter ( 6559): └WidgetsApp([GlobalObjectKey _MaterialAppState(1009803148)]; state: _WidgetsAppState(552902158))
I/flutter ( 6559): └CheckedModeBanner()
I/flutter ( 6559): └Banner()
I/flutter ( 6559): └CustomPaint(renderObject: RenderCustomPaint)
I/flutter ( 6559): └DefaultTextStyle(inherit: true; color: Color(0xd0ff0000); family: "monospace"; size: 48.0; weight: 900; decoration: double Color(0xffffff00) TextDecoration.underline)
I/flutter ( 6559): └MediaQuery(MediaQueryData(size: Size(411.4, 683.4), devicePixelRatio: 2.625, textScaleFactor: 1.0, padding: EdgeInsets(0.0, 24.0, 0.0, 0.0)))
I/flutter ( 6559): └LocaleQuery(null)
I/flutter ( 6559): └Title(color: Color(0xff2196f3))
... #省略剩餘內容
這是一個“扁平化”的樹,顯示了通過各種構建函數投影的所有widget(如果你在widget樹的根中調用toStringDeepwidget
,這是你獲得的樹)。 你會看到很多在你的應用源代碼中沒有出現的widget,因爲它們是被框架中widget的build()
函數插入的。例如,InkFeature
是Material widget的一個實現細節 。
當按鈕從被按下變爲被釋放時debugDumpApp()被調用,FlatButton對象同時調用setState()
,並將自己標記爲"dirty"。 這就是爲什麼如果你看轉儲,你會看到特定的對象標記爲“dirty”。您還可以查看已註冊了哪些手勢監聽器; 在這種情況下,一個單一的GestureDetector被列出,並且監聽“tap”手勢(“tap”是TapGestureDetector
的toStringShort
函數輸出的)
如果您編寫自己的widget,則可以通過覆蓋debugFillProperties()
來添加信息。 將DiagnosticsProperty對象作爲方法參數,並調用父類方法。 該函數是該toString
方法用來填充小部件描述信息的。
渲染樹
如果您嘗試調試佈局問題,那麼Widget樹可能不夠詳細。在這種情況下,您可以通過調用debugDumpRenderTree()
轉儲渲染樹。 正如debugDumpApp()
,除佈局或繪製階段外,您可以隨時調用此函數。作爲一般規則,從frame 回調 或事件處理器中調用它是最佳解決方案。
要調用debugDumpRenderTree()
,您需要添加import'package:flutter/rendering.dart';
到您的源文件。
上面這個小例子的輸出結果如下所示:
I/flutter ( 6559): RenderView
I/flutter ( 6559): │ debug mode enabled - android
I/flutter ( 6559): │ window size: Size(1080.0, 1794.0) (in physical pixels)
I/flutter ( 6559): │ device pixel ratio: 2.625 (physical pixels per logical pixel)
I/flutter ( 6559): │ configuration: Size(411.4, 683.4) at 2.625x (in logical pixels)
I/flutter ( 6559): │
I/flutter ( 6559): └─child: RenderCustomPaint
I/flutter ( 6559): │ creator: CustomPaint ← Banner ← CheckedModeBanner ←
I/flutter ( 6559): │ WidgetsApp-[GlobalObjectKey _MaterialAppState(1009803148)] ←
I/flutter ( 6559): │ Theme ← AnimatedTheme ← ScrollConfiguration ← MaterialApp ←
I/flutter ( 6559): │ [root]
I/flutter ( 6559): │ parentData: <none>
I/flutter ( 6559): │ constraints: BoxConstraints(w=411.4, h=683.4)
I/flutter ( 6559): │ size: Size(411.4, 683.4)
... # 省略
這是根RenderObject
對象的toStringDeep
函數的輸出。
當調試佈局問題時,關鍵要看的是size
和constraints
字段。約束沿着樹向下傳遞,尺寸向上傳遞。
如果您編寫自己的渲染對象,則可以通過覆蓋debugFillProperties()
將信息添加到轉儲。 將DiagnosticsProperty對象作爲方法的參數,並調用父類方法。
Layer樹
讀者可以理解爲渲染樹是可以分層的,而最終繪製需要將不同的層合成起來,而Layer則是繪製時需要合成的層,如果您嘗試調試合成問題,則可以使用debugDumpLayerTree()
。對於上面的例子,它會輸出:
I/flutter : TransformLayer
I/flutter : │ creator: [root]
I/flutter : │ offset: Offset(0.0, 0.0)
I/flutter : │ transform:
I/flutter : │ [0] 3.5,0.0,0.0,0.0
I/flutter : │ [1] 0.0,3.5,0.0,0.0
I/flutter : │ [2] 0.0,0.0,1.0,0.0
I/flutter : │ [3] 0.0,0.0,0.0,1.0
I/flutter : │
I/flutter : ├─child 1: OffsetLayer
I/flutter : │ │ creator: RepaintBoundary ← _FocusScope ← Semantics ← Focus-[GlobalObjectKey MaterialPageRoute(560156430)] ← _ModalScope-[GlobalKey 328026813] ← _OverlayEntry-[GlobalKey 388965355] ← Stack ← Overlay-[GlobalKey 625702218] ← Navigator-[GlobalObjectKey _MaterialAppState(859106034)] ← Title ← ⋯
I/flutter : │ │ offset: Offset(0.0, 0.0)
I/flutter : │ │
I/flutter : │ └─child 1: PictureLayer
I/flutter : │
I/flutter : └─child 2: PictureLayer
這是根Layer
的toStringDeep
輸出的。
根部的變換是應用設備像素比的變換; 在這種情況下,每個邏輯像素代表3.5個設備像素。
RepaintBoundary
widget在渲染樹的層中創建了一個RenderRepaintBoundary
。這用於減少需要重繪的需求量。
語義
您還可以調用debugDumpSemanticsTree()
獲取語義樹(呈現給系統可訪問性API的樹)的轉儲。 要使用此功能,必須首先啓用輔助功能,例如啓用系統輔助工具或SemanticsDebugger
(下面討論)。
對於上面的例子,它會輸出:
I/flutter : SemanticsNode(0; Rect.fromLTRB(0.0, 0.0, 411.4, 683.4))
I/flutter : ├SemanticsNode(1; Rect.fromLTRB(0.0, 0.0, 411.4, 683.4))
I/flutter : │ └SemanticsNode(2; Rect.fromLTRB(0.0, 0.0, 411.4, 683.4); canBeTapped)
I/flutter : └SemanticsNode(3; Rect.fromLTRB(0.0, 0.0, 411.4, 683.4))
I/flutter : └SemanticsNode(4; Rect.fromLTRB(0.0, 0.0, 82.0, 36.0); canBeTapped; "Dump App")
調度
要找出相對於幀的開始/結束事件發生的位置,可以切換debugPrintBeginFrameBanner
和debugPrintEndFrameBanner
布爾值以將幀的開始和結束打印到控制檯。
例如:
I/flutter : ▄▄▄▄▄▄▄▄ Frame 12 30s 437.086ms ▄▄▄▄▄▄▄▄
I/flutter : Debug print: Am I performing this work more than once per frame?
I/flutter : Debug print: Am I performing this work more than once per frame?
I/flutter : ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
debugPrintScheduleFrameStacks
還可以用來打印導致當前幀被調度的調用堆棧。
可視化調試
您也可以通過設置debugPaintSizeEnabled
爲true
以可視方式調試佈局問題。 這是來自rendering
庫的布爾值。它可以在任何時候啓用,並在爲true時影響繪製。 設置它的最簡單方法是在void main()
的頂部設置。
當它被啓用時,所有的盒子都會得到一個明亮的深青色邊框,padding(來自widget如Padding)顯示爲淺藍色,子widget周圍有一個深藍色框, 對齊方式(來自widget如Center和Align)顯示爲黃色箭頭. 空白(如沒有任何子節點的Container)以灰色顯示。
debugPaintBaselinesEnabled
做了類似的事情,但對於具有基線的對象,文字基線以綠色顯示,表意(ideographic)基線以橙色顯示。
debugPaintPointersEnabled
標誌打開一個特殊模式,任何正在點擊的對象都會以深青色突出顯示。 這可以幫助您確定某個對象是否以某種不正確的方式進行hit測試(Flutter檢測點擊的位置是否有能響應用戶操作的widget),例如,如果它實際上超出了其父項的範圍,首先不會考慮通過hit測試。
如果您嘗試調試合成圖層,例如以確定是否以及在何處添加RepaintBoundary
widget,則可以使用debugPaintLayerBordersEnabled
標誌, 該標誌用橙色或輪廓線標出每個層的邊界,或者使用debugRepaintRainbowEnabled
標誌, 只要他們重繪時,這會使該層被一組旋轉色所覆蓋。
所有這些標誌只能在調試模式下工作。通常,Flutter框架中以“debug...
” 開頭的任何內容都只能在調試模式下工作。
調試動畫
調試動畫最簡單的方法是減慢它們的速度。爲此,請將timeDilation
變量(在scheduler庫中)設置爲大於1.0的數字,例如50.0。 最好在應用程序啓動時只設置一次。如果您在運行中更改它,尤其是在動畫運行時將其值改小,則在觀察時可能會出現倒退,這可能會導致斷言命中,並且這通常會干擾我們的開發工作。
調試性能問題
要了解您的應用程序導致重新佈局或重新繪製的原因,您可以分別設置debugPrintMarkNeedsLayoutStacks
和 debugPrintMarkNeedsPaintStacks
標誌。 每當渲染盒被要求重新佈局和重新繪製時,這些都會將堆棧跟蹤記錄到控制檯。如果這種方法對您有用,您可以使用services
庫中的debugPrintStack()
方法按需打印堆棧痕跡。
統計應用啓動時間
要收集有關Flutter應用程序啓動所需時間的詳細信息,可以在運行flutter run
時使用trace-startup
和profile
選項。
$ flutter run --trace-startup --profile
跟蹤輸出保存爲start_up_info.json
,在Flutter工程目錄在build目錄下。輸出列出了從應用程序啓動到這些跟蹤事件(以微秒捕獲)所用的時間:
- 進入Flutter引擎時.
- 展示應用第一幀時.
- 初始化Flutter框架時.
- 完成Flutter框架初始化時.
如 :
{
"engineEnterTimestampMicros": 96025565262,
"timeToFirstFrameMicros": 2171978,
"timeToFrameworkInitMicros": 514585,
"timeAfterFrameworkInitMicros": 1657393
}
跟蹤Dart代碼性能
要執行自定義性能跟蹤和測量Dart任意代碼段的wall/CPU時間(類似於在Android上使用systrace)。 使用dart:developer
的Timeline工具來包含你想測試的代碼塊,例如:
Timeline.startSync('interesting function');
// iWonderHowLongThisTakes();
Timeline.finishSync();
然後打開你應用程序的Observatory timeline頁面,在“Recorded Streams”中選擇‘Dart’複選框,並執行你想測量的功能。
刷新頁面將在Chrome的跟蹤工具中顯示應用按時間順序排列的timeline記錄。
請確保運行flutter run
時帶有--profile
標誌,以確保運行時性能特徵與您的最終產品差異最小。