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