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