有各種各樣的工具和功能來幫助調(diào)試 Flutter 應用程序。
在運行應用程序前,請運行flutter analyze
測試你的代碼。這個工具是一個靜態(tài)代碼檢查工具,它是dartanalyzer
工具的一個包裝,主要用于分析代碼并幫助開發(fā)者發(fā)現(xiàn)可能的錯誤,比如,Dart 分析器大量使用了代碼中的類型注釋來幫助追蹤問題,避免var
、無類型的參數(shù)、無類型的列表文字等。
如果你使用 IntelliJ 的 Flutter插件,那么分析器在打開 IDE 時就已經(jīng)自動啟用了,如果讀者使用的是其它 IDE,強烈建議讀者啟用 Dart 分析器,因為在大多數(shù)時候,Dart 分析器可以在代碼運行前發(fā)現(xiàn)大多數(shù)問題。
如果我們使用flutter run
啟動應用程序,那么當它運行時,我們可以打開 Observatory 工具的 Web 頁面,例如 Observatory 默認監(jiān)聽http://127.0.0.1:8100/ (opens new window),可以在瀏覽器中直接打開該鏈接。直接使用語句級單步調(diào)試器連接到您的應用程序。如果您使用的是 IntelliJ,則還可以使用其內(nèi)置的調(diào)試器來調(diào)試您的應用程序。
Observatory 同時支持分析、檢查堆等。有關 Observatory 的更多信息請參考Observatory 文檔 (opens new window)。
如果您使用 Observatory 進行分析,請確保通過--profile
選項來運行flutter run
命令來運行應用程序。 否則,配置文件中將出現(xiàn)的主要問題將是調(diào)試斷言,以驗證框架的各種不變量(請參閱下面的“調(diào)試模式斷言”)。
debugger()
聲明
當使用 Dart Observatory(或另一個 Dart 調(diào)試器,例如 IntelliJ IDE 中的調(diào)試器)時,可以使用該debugger()
語句插入編程式斷點。要使用這個,你必須添加import 'dart:developer';
到相關文件頂部。
debugger()
語句采用一個可選when
參數(shù),您可以指定該參數(shù)僅在特定條件為真時中斷,如下所示:
void someFunction(double offset) {
debugger(when: offset > 30.0);
// ...
}
print
、debugPrint
、flutter logs
Dart print()
功能將輸出到系統(tǒng)控制臺,您可以使用flutter logs
來查看它(基本上是一個包裝adb logcat
)。
如果你一次輸出太多,那么Android有時會丟棄一些日志行。為了避免這種情況,您可以使用 Flutter的foundation
庫中的debugPrint()
(opens new window)。 這是一個封裝 print,它將輸出限制在一個級別,避免被 Android 內(nèi)核丟棄。
Flutter 框架中的許多類都有toString
實現(xiàn)。按照慣例,這些輸出通常包括對象的runtimeType
單行輸出,通常在表單中 ClassName(more information about this instance…)。 樹中使用的一些類也具有toStringDeep
,從該點返回整個子樹的多行描述。已一些具有詳細信息toString
的類會實現(xiàn)一個toStringShort
,它只返回對象的類型或其他非常簡短的(一個或兩個單詞)描述。
在 Flutter 應用調(diào)試過程中,Dart assert
語句被啟用,并且 Flutter 框架使用它來執(zhí)行許多運行時檢查來驗證是否違反一些不可變的規(guī)則。
當一個不可變的規(guī)則被違反時,它被報告給控制臺,并帶有一些上下文信息來幫助追蹤問題的根源。
要關閉調(diào)試模式并使用發(fā)布模式,請使用flutter run --release
運行您的應用程序。 這也關閉了 Observatory 調(diào)試器。一個中間模式可以關閉除 Observatory 之外所有調(diào)試輔助工具的,稱為“profile mode”,用--profile
替代--release
即可。
Flutter框架的每一層都提供了將其當前狀態(tài)或事件轉儲(dump)到控制臺(使用debugPrint
)的功能。
要轉儲 Widgets 樹的狀態(tài),請調(diào)用debugDumpApp()
(opens new window)。 只要應用程序已經(jīng)構建了至少一次(即在調(diào)用build()
之后的任何時間),您可以在應用程序未處于構建階段(即,不在build()
方法內(nèi)調(diào)用 )的任何時間調(diào)用此方法(在調(diào)用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'),
),
),
);
}
}
…會輸出這樣的內(nèi)容(精確的細節(jié)會根據(jù)框架的版本、設備的大小等等而變化):
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))
... #省略剩余內(nèi)容
這是一個“扁平化”的樹,顯示了通過各種構建函數(shù)投影的所有 widget(如果你在 widget 樹的根中調(diào)用toStringDeepwidget
,這是你獲得的樹)。 你會看到很多在你的應用源代碼中沒有出現(xiàn)的 widget,因為它們是被框架中 widget 的build()
函數(shù)插入的。例如,InkFeature
(opens new window)是 Material widget 的一個實現(xiàn)細節(jié) 。
當按鈕從被按下變?yōu)楸会尫艜r debugDumpApp() 被調(diào)用,F(xiàn)latButton 對象同時調(diào)用setState()
,并將自己標記為"dirty"。 這就是為什么如果你看轉儲,你會看到特定的對象標記為“dirty”。您還可以查看已注冊了哪些手勢監(jiān)聽器; 在這種情況下,一個單一的 GestureDetector 被列出,并且監(jiān)聽“tap”手勢(“tap”是TapGestureDetector
的toStringShort
函數(shù)輸出的)
如果您編寫自己的 widget,則可以通過覆蓋debugFillProperties()
(opens new window)來添加信息。 將 DiagnosticsProperty (opens new window)對象作為方法參數(shù),并調(diào)用父類方法。 該函數(shù)是該toString
方法用來填充小部件描述信息的。
如果您嘗試調(diào)試布局問題,那么 Widget 樹可能不夠詳細。在這種情況下,您可以通過調(diào)用debugDumpRenderTree()
轉儲渲染樹。 正如debugDumpApp()
,除布局或繪制階段外,您可以隨時調(diào)用此函數(shù)。作為一般規(guī)則,從 frame 回調(diào) (opens new window)或事件處理器中調(diào)用它是最佳解決方案。
要調(diào)用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
函數(shù)的輸出。
當調(diào)試布局問題時,關鍵要看的是size
和constraints
字段。約束沿著樹向下傳遞,尺寸向上傳遞。
如果您編寫自己的渲染對象,則可以通過覆蓋debugFillProperties()
(opens new window)將信息添加到轉儲。 將 DiagnosticsProperty (opens new window)對象作為方法的參數(shù),并調(diào)用父類方法。
讀者可以理解為渲染樹是可以分層的,而最終繪制需要將不同的層合成起來,而 Layer 則是繪制時需要合成的層,如果您嘗試調(diào)試合成問題,則可以使用debugDumpLayerTree()
(opens new window)。對于上面的例子,它會輸出:
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 在渲染樹的層中創(chuàng)建了一個RenderRepaintBoundary
。這用于減少需要重繪的需求量。
您還可以調(diào)用debugDumpSemanticsTree()
(opens new window)獲取語義樹(呈現(xiàn)給系統(tǒng)可訪問性 API 的樹)的轉儲。 要使用此功能,必須首先啟用輔助功能,例如啟用系統(tǒng)輔助工具或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")
要找出相對于幀的開始/結束事件發(fā)生的位置,可以切換debugPrintBeginFrameBanner
(opens new window)和debugPrintEndFrameBanner
(opens new window)布爾值以將幀的開始和結束打印到控制臺。
例如:
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
(opens new window)還可以用來打印導致當前幀被調(diào)度的調(diào)用堆棧。
您也可以通過設置debugPaintSizeEnabled
為true
以可視方式調(diào)試布局問題。 這是來自rendering
庫的布爾值。它可以在任何時候啟用,并在為 true 時影響繪制。 設置它的最簡單方法是在void main()
的頂部設置。
當它被啟用時,所有的盒子都會得到一個明亮的深青色邊框,padding(來自 widget 如 Padding)顯示為淺藍色,子 widget 周圍有一個深藍色框, 對齊方式(來自 widget 如 Center 和 Align)顯示為黃色箭頭. 空白(如沒有任何子節(jié)點的 Container)以灰色顯示。
debugPaintBaselinesEnabled
(opens new window)做了類似的事情,但對于具有基線的對象,文字基線以綠色顯示,表意(ideographic)基線以橙色顯示。
debugPaintPointersEnabled
(opens new window)標志打開一個特殊模式,任何正在點擊的對象都會以深青色突出顯示。 這可以幫助您確定某個對象是否以某種不正確的方式進行 hit 測試(Flutter 檢測點擊的位置是否有能響應用戶操作的 widget),例如,如果它實際上超出了其父項的范圍,首先不會考慮通過hit測試。
如果您嘗試調(diào)試合成圖層,例如以確定是否以及在何處添加RepaintBoundary
widget,則可以使用debugPaintLayerBordersEnabled
(opens new window)標志, 該標志用橙色或輪廓線標出每個層的邊界,或者使用debugRepaintRainbowEnabled
(opens new window)標志, 只要他們重繪時,這會使該層被一組旋轉色所覆蓋。
所有這些標志只能在調(diào)試模式下工作。通常,F(xiàn)lutter 框架中以“debug...
” 開頭的任何內(nèi)容都只能在調(diào)試模式下工作。
調(diào)試動畫最簡單的方法是減慢它們的速度。為此,請將timeDilation
(opens new window)變量(在scheduler庫中)設置為大于1.0的數(shù)字,例如50.0。 最好在應用程序啟動時只設置一次。如果您在運行中更改它,尤其是在動畫運行時將其值改小,則在觀察時可能會出現(xiàn)倒退,這可能會導致斷言命中,并且這通常會干擾我們的開發(fā)工作。
要了解您的應用程序導致重新布局或重新繪制的原因,您可以分別設置debugPrintMarkNeedsLayoutStacks
(opens new window)和 debugPrintMarkNeedsPaintStacks
(opens new window)標志。 每當渲染盒被要求重新布局和重新繪制時,這些都會將堆棧跟蹤記錄到控制臺。如果這種方法對您有用,您可以使用services
庫中的debugPrintStack()
方法按需打印堆棧痕跡。
要收集有關 Flutter 應用程序啟動所需時間的詳細信息,可以在運行flutter run
時使用trace-startup
和profile
選項。
$ flutter run --trace-startup --profile
跟蹤輸出保存為start_up_info.json
,在 Flutter 工程目錄在 build 目錄下。輸出列出了從應用程序啟動到這些跟蹤事件(以微秒捕獲)所用的時間:
如 :
{
"engineEnterTimestampMicros": 96025565262,
"timeToFirstFrameMicros": 2171978,
"timeToFrameworkInitMicros": 514585,
"timeAfterFrameworkInitMicros": 1657393
}
要執(zhí)行自定義性能跟蹤和測量 Dart 任意代碼段的 wall/CPU 時間(類似于在 Android 上使用systrace (opens new window))。 使用dart:developer
的Timeline (opens new window)工具來包含你想測試的代碼塊,例如:
Timeline.startSync('interesting function');
// iWonderHowLongThisTakes();
Timeline.finishSync();
然后打開你應用程序的 Observatory timeline 頁面,在“Recorded Streams”中選擇‘Dart’復選框,并執(zhí)行你想測量的功能。
刷新頁面將在Chrome的跟蹤工具 (opens new window)中顯示應用按時間順序排列的 timeline 記錄。
請確保運行flutter run
時帶有--profile
標志,以確保運行時性能特征與您的最終產(chǎn)品差異最小。
更多建議: