Flutter實戰(zhàn) 調(diào)試Flutter應用

2021-03-06 17:03 更新

有各種各樣的工具和功能來幫助調(diào)試 Flutter 應用程序。

#Dart 分析器

在運行應用程序前,請運行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ù)問題。

#Dart Observatory (語句級的單步調(diào)試和分析器)

如果我們使用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、debugPrintflutter 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,它只返回對象的類型或其他非常簡短的(一個或兩個單詞)描述。

#調(diào)試模式斷言

在 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即可。

#調(diào)試應用程序層

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

#Widget 樹

要轉儲 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”是TapGestureDetectortoStringShort函數(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)試布局問題時,關鍵要看的是sizeconstraints字段。約束沿著樹向下傳遞,尺寸向上傳遞。

如果您編寫自己的渲染對象,則可以通過覆蓋debugFillProperties() (opens new window)將信息添加到轉儲。 將 DiagnosticsProperty (opens new window)對象作為方法的參數(shù),并調(diào)用父類方法。

#Layer樹

讀者可以理解為渲染樹是可以分層的,而最終繪制需要將不同的層合成起來,而 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

這是根LayertoStringDeep輸出的。

根部的變換是應用設備像素比的變換; 在這種情況下,每個邏輯像素代表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")

#調(diào)度

要找出相對于幀的開始/結束事件發(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)用堆棧。

#可視化調(diào)試

您也可以通過設置debugPaintSizeEnabledtrue以可視方式調(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)試動畫

調(diào)試動畫最簡單的方法是減慢它們的速度。為此,請將timeDilation (opens new window)變量(在scheduler庫中)設置為大于1.0的數(shù)字,例如50.0。 最好在應用程序啟動時只設置一次。如果您在運行中更改它,尤其是在動畫運行時將其值改小,則在觀察時可能會出現(xiàn)倒退,這可能會導致斷言命中,并且這通常會干擾我們的開發(fā)工作。

#調(diào)試性能問題

要了解您的應用程序導致重新布局或重新繪制的原因,您可以分別設置debugPrintMarkNeedsLayoutStacks (opens new window)和 debugPrintMarkNeedsPaintStacks (opens new window)標志。 每當渲染盒被要求重新布局和重新繪制時,這些都會將堆棧跟蹤記錄到控制臺。如果這種方法對您有用,您可以使用services庫中的debugPrintStack()方法按需打印堆棧痕跡。

#統(tǒng)計應用啟動時間

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

$ flutter run --trace-startup --profile

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

  • 進入 Flutter 引擎時.
  • 展示應用第一幀時.
  • 初始化 Flutter 框架時.
  • 完成 Flutter 框架初始化時.

如 :

{
  "engineEnterTimestampMicros": 96025565262,
  "timeToFirstFrameMicros": 2171978,
  "timeToFrameworkInitMicros": 514585,
  "timeAfterFrameworkInitMicros": 1657393
}

#跟蹤Dart代碼性能

要執(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)品差異最小。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號