初略講解如何調試Flutter應用

1、Dart Analyzer(分析器)

在使用flutter run命令來運行應用程序以前,請運行flutter analyze命令來檢測你的代碼,這個命令是Dart Analyzer(分析器)的一個包裝,它將分析你的代碼並幫助你發現可能出現的錯誤。由於Dart Analyzer分析器大量使用了代碼中的類型註釋來幫助追蹤問題,因此筆者鼓勵你們在任何地方任什麼時候候都來使用它檢測你的代碼,從而避免var、無類型的參數和無類型的列表文字等問題,能夠說它是追蹤問題的最快的方式。html

2、Dart Observatory(語句級的單步調式和分析器)

使用flutter run命令運行應用程序,運行的時候,在控制檯能夠看到一個Observatory URL(如http://127.0.0.1:8100),這個url能夠經過瀏覽器打開,直接用語句級單步調試程序鏈接到你的應用程序。若是你使用的是IntelliJ,則可使用其內置的調式器(運行的時候選擇debug按鈕)來調試你的應用程序。android

Observatory同時支持分析、檢查堆等,有關Observatory的更多信息請參考Observatory文檔git

若是使用Observatory進行分析,請使用flutter run --profile命令運行應用程序;不然,配置文件中出現的主要問題將是調試斷言,以驗證框架的各類不變量(請參閱下面的「3、調試模式斷言」github

一、debugger()

當使用Dart Observatory或另一個Dart調試器(如:IntelliJ IDE中的調試器)時,可使用debugger語句插入編程式斷點,要使用該命令,必須添加import 'dart:developer';到相關文件頂部。編程

debugger()語句帶有一個可選when參數,能夠指定該參數僅在特定條件爲真時中斷,代碼以下:json

void someFunction(double offset) {
  debugger(when: offset > 30.0);
  // ...
}
複製代碼

二、printdebugPrintflutter logs

使用Dart的print()方法將日誌打印到系統控制檯上,咱們可使用flutter logs來查閱日誌,這個命令基本上是對adb logcat命令作了一層封裝。api

若是日誌一次輸出太多,那麼Android的作法是設置日誌優先級或者有時會丟棄一些日誌行,爲了不這種狀況,可使用Flutter的foundation庫中的debugPrint()方法。這個方法對print()方法作了一層包裝,它將輸出限制在一個級別,避免被Android內核丟棄。瀏覽器

Flutter框架中的許多類都有對toString的實現,按照慣例,這些輸出一般包括runtimeType對象的單行輸出,一般在ClassName(more information about this instance…)表格中。樹中使用的某些類也具備從該點返回整個子樹的多行描述的toStringDeep方法。一些具備打印詳細日誌的toString()方法的類,會實現一個相應的只返回類型或者對對象只有一兩個詞語簡短描述的toStringShort()方法。bash

3、調試模式斷言

在開發過程當中,強烈建議你使用Flutter的「調試(debug)」模式,有時也稱爲「檢查(checked)」模式(注意:Dart2.0後「checked」模式被廢除,可使用「strong」模式)。若是你使用flutter run運行程序,「調試」模式是默認的,在這種模式下,Dart assert語句被啓用,Flutter框架使用它來執行許多運行時檢查、驗證賦值是否合法。當一個賦值不合法時,它會向控制檯報告,並提供一些上下文信息來幫助追蹤問題的根源。app

要關閉「調試(debug)」模式並使用「發佈(release)」模式,請使用flutter run --release運行你的應用程序,不過這樣也關閉了Observatory調式器,一箇中間模式能夠關閉除Observatory調試器以外的全部調試輔助工具,稱爲「profile」模式,用--profile替代--release便可。

4、調試應用程序層

Flutter框架的每一層都提供了將其當前狀態或事件,轉儲(dump)到控制檯(使用debugPrint)的功能。

Widget層

要轉儲Widgets(控件)庫的狀態,請調用debugDumpApp()方法。只要應用程序至少構建了一次(即,在調用runApp()以後的任什麼時候間),就能夠在應用程序未處於運行構建階段(即,不在build()方法內調用)的任什麼時候間調用此方法。小例子(這個小例子下面還會使用到)

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'),
        ),
      ),
    );
  }
}
複製代碼

應用程序運行起來以後,點擊「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))
... #省略剩餘內容
複製代碼

這是一個「扁平化」的樹,顯示經過各類build函數投影的全部widget(這是在widget樹的根上調用toStringDeep時得到的樹)。從上面的輸入日誌你將看到不少在應用程序源代碼中沒有出現過的widget,由於它們是被框架中widget的build函數插入的。如:InkFeature是Material widget的一個實現細節。

當「Dump App」按鈕從被按下到被釋放時debugDumpApp()方法將被調用,FlatButton對象同時調用setState(),並將本身標記爲「dirty」,這就是爲何當你查看轉儲時,會看到特定的對象標記爲「dirty」。你還能夠查看哪些手勢監聽器(GestureDetector)已註冊了,在這種狀況下,一個單一的GestureDetector被列出,它只監聽「tap」手勢(「tap」是TapGestureDetectortoStringShort()方法的輸出)。

若是編寫本身的widget,則能夠經過重寫debugFillProperties()方法來添加信息到轉儲,並將DiagnosticsProperty對象做爲方法的參數進行傳遞,同時調用父類方法,這個方法是toString方法用來填充widget描述信息的。代碼以下:

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
  super.debugFillProperties(properties);
  properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
}
複製代碼

渲染層

若是你嘗試調試佈局問題,那麼Widgets(控件)層的樹可能不夠詳細,在這種狀況下,你能夠經過調用debugDumpRenderTree()轉儲渲染樹。和debugDumpApp()用法同樣,除了layout(佈局)或paint(繪畫)階段以外,你能夠隨時調用它,約定俗成,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(約束)字段,約束沿着樹向下傳遞,大小則向上傳遞。

例如:在上面的轉儲中,你能夠看到窗口大小,Size(411.4, 683.4),它用於強制RenderPositionedBox下的全部渲染框成爲屏幕的大小,並具備BoxConstraints(w=411.4, h=683.4)的約束。從RenderPositionedBox的轉儲中看到是由Center組件(由creator字段描述的)建立的,設置其子控件的約束爲:BoxConstraints(0.0<=w<=411.4,0.0<=h<=683.4)。子控件RenderPadding進一步插入這些約束以確保有足夠的空間填充,padding值爲EdgeInsets(16.0, 0.0, 16.0, 0.0),所以RenderConstrainedBox具備一個BoxConstraints(0.0<=w<=395.4, 0.0<=h<=667.4)約束。該creator字段告訴咱們,此對象多是其FlatButton定義的一部分,它在內容上設置的最小寬度爲88px,具體高度爲36.0px(這是Material Design設計規範中FlatButton類的尺寸標準)。

最內部的RenderPositionedBox再次釋放約束,此次是將按鈕中的文本居中, RenderParagraph根據其內容選擇其大小,若是如今按照size繼續往下查看,你會看到文本的尺寸是如何影響其按鈕的框的寬度的,由於它們會根據子控件的框的尺寸自行調整大小。

另外一種須要注意的是每一個box(盒子容器)描述的「relayoutSubtreeRoot」部分,由於它在告訴你有多少「祖先」在某種程度上依賴於這個元素的大小。好比RenderParagraph有一個relayoutSubtreeRoot=up8,那麼這就意味着當它RenderParagraph被標記爲「dirty」時,它的八個「祖先」也必須被標記爲「dirty」,由於它們可能受到新尺寸的影響。

若是編寫本身的渲染對象,則能夠經過覆寫debugFillProperties()方法將信息添加到轉儲,並將DiagnosticsProperty對象做爲方法的參數進行傳遞,同時調用父類方法。

圖層

若是你嘗試調試合成問題,則可使用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 控件在渲染層中建立了一個新的圖層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")
複製代碼

調度

要找出相對於幀的開始/結束事件發生的位置,能夠切換debugPrintBeginFrameBannerdebugPrintEndFrameBanner的boolean(布爾值)來將幀的開始和結束打印到控制檯上。例如:

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還能夠用來打印致使當前幀被調度的調用堆棧。

5、可視化調試

你也能夠經過將debugPaintSizeEnabled設置爲true,以可視化方式調試佈局問題。這是來自rendering(渲染)庫中的一個布爾值變量,它能夠在任什麼時候候啓用,並在爲true時影響全部的繪製。最簡單的辦法是在void main()主函數頂部入口去設置它:

//add import to rendering library
import 'package:flutter/rendering.dart';

void main() {
  debugPaintSizeEnabled=true;
  runApp(MyApp());
}
複製代碼

當打開可視化調試時,全部的盒子都會獲得一個明亮的深青色邊框,padding(來自Widget如Padding)顯示爲淺藍色,子控件的內邊距會有一個明亮漸變的深藍色邊框,對齊方式(來自Widget如Center和Align)顯示爲黃色箭頭,沒有任何子節點的Container顯示爲灰色。

debugPaintBaselinesEnabled的功能相似於對象的基準線,字母基線顯示亮綠色,表意基線以橙色顯示。

debugPaintPointersEnabled標記位會打開一種特殊模式,在此模式下,被點擊的任何對象都會以深青色突出顯示。這能夠幫助你肯定某個對象是否以某種不正確的方式進行hit(命中)測試(Flutter檢測點擊的位置是否有能響應用戶操做的widget),例如,某個對象實際上超出了其父對象的範圍以外,那麼就不會第一時間被考慮經過hit(命中)測試。

若是你嘗試調試混合圖層,例如,以肯定是否以及在何處添加RepaintBoundary控件,則可使用debugPaintLayerBordersEnabled標記位,該標記用橙色或輪廓線繪製出每一個圖層的邊界,或者使用debugRepaintRainbowEnabled標記位,當圖層從新繪製時,它將讓圖層顯示旋轉的色彩。

全部這些標誌位只在調試模式下工做,通常來講,在Flutter框架中,任何以「debug...」開頭的變量或方法,都只能在調試模式下有效。

6、調試動畫

調試動畫最簡單的方法是放慢它們的速度。爲此,請將timeDilation變量(在scheduler庫中)設置爲大於1.0的數字,例如50.0。最好在應用程序啓動時只設置一次,若是在運行中更改它,尤爲是在動畫運行時將其值變小,則框架可能會觀察到時間倒退,這可能會致使斷言而且一般會干擾你的工做。

7、調試性能問題

要了解致使你的應用程序從新佈局或從新繪製的緣由,能夠分別設置debugPrintMarkNeedsLayoutStacksdebugPrintMarkNeedsPaintStacks標誌。每當渲染框被要求從新佈局或從新繪製時,這些都會隨時將堆棧跟蹤日誌打印到控制檯上。若是這種方法對你有用,你可使用services庫中的debugPrintStack()方法按需打印堆棧痕跡。

統計應用啓動時間

要收集有關Flutter應用程序啓動所需時間的詳細信息,能夠在運行flutter run命令時使用trace-startupprofile選項。

$ flutter run --trace-startup --profile
複製代碼

跟蹤日誌被保存在你的Flutter工程目錄下的build目錄下的start_up_info.json文件中。日誌輸出列出了從應用程序啓動到這些跟蹤事件(以微秒捕獲)所用的時間:

  • 進入Flutter引擎代碼的時間
  • 繪製應用第一幀的時間
  • 初始化Flutter框架的時間
  • 完成Flutter框架初始化的時間

例如:

{
  "engineEnterTimestampMicros": 96025565262,
  "timeToFirstFrameMicros": 2171978,
  "timeToFrameworkInitMicros": 514585,
  "timeAfterFrameworkInitMicros": 1657393
}
複製代碼

跟蹤Dart代碼性能

要執行自定義性能跟蹤並測量Dart任意代碼塊的wall/CPU時間(相似於在Android上使用systrace)。可使用dart:developerTimeline工具來包含你想測試的代碼塊,例如:

Timeline.startSync('interesting function');
// iWonderHowLongThisTakes();
Timeline.finishSync();
複製代碼

而後打開你的應用程序的Observatory timeline頁面,在「Recorded Streams」中選擇‘Dart’複選框,並執行你想測試的功能。刷新該頁面,將會在Chrome瀏覽器的跟蹤工具中按時間順序顯示應用程序的時間軸記錄。

務必使用flutter run --profile命令運行應用程序,以確保運行時性能特徵與你的最終產品的性能差別最小。

8、性能覆蓋圖

要得到應用程序性能圖形視圖,請將MaterialApp構造函數的showPerformanceOverlay參數設置爲trueWidgetsApp構造函數也有相似的參數(若是你沒有使用MaterialApp或者WidgetsApp,你能夠經過將你的應用程序封裝在一個堆棧中,調用new PerformanceOverlay.allEnabled()方法建立一個控件放在堆棧上來得到相同的效果)。

這將顯示兩個圖表,第一個是GPU線程花費的時間,第二個是CPU線程花費的時間。圖中的白線以16ms增量沿縱軸顯示,若是圖表通過其中的一條白線,那麼你的運行頻率(速度)低於60Hz,橫軸表明幀。該圖表只會在應用程序繪製時更新,因此若是它處於空閒狀態,該圖表將中止移動。

這個操做必定是在發佈模式下完成的,由於在調試模式下,會故意犧牲性能來換取有助於開發調試的功能,如assert聲明,這些都是很是耗時的,所以結果將會產生誤導。

9、Material網格

當咱們開發實現Material Design的應用程序時,應用程序上會覆蓋一個幫助驗證對齊的Material Design基線網格。爲此,在調試模式下,將MaterialApp構造函數debugShowMaterialGrid參數設爲true,將會覆蓋這樣一個網格。也能夠直接使用GridPaper控件在非Material應用程序上覆蓋這樣的網格。

相關文章
相關標籤/搜索