第二章: 第一个Flutter应用 2.7 调试Flutter应用

跟随《Flutter实战·第二版》学习,建议直接看原书

有各种各样的工具和功能来帮助调试Flutter应用程序。

日志与断点

debugger声明
当使用Dart Observatory(或另一个Dart调试器,例如IntelliJ IDE中的调试器)时,可以使用该debugger()语句插入编程式断点。要使用这个,你必须添加import 'dart:developer';到相关文件顶部。

debugger()语句采用一个可选when参数,我们可以指定该参数仅在特定条件为真时中断,如下所示:

void someFunction(double offset) {
  debugger(when: offset > 30.0);
  // ...
}
print、debugPrint、flutter logs

Dart print()功能将输出到系统控制台,我们可以使用flutter logs来查看它(基本上是一个包装adb logcat)。
如果你一次输出太多,那么Android有时会丢弃一些日志行。为了避免这种情况,我们可以使用Flutter的foundation库中的debugPrint() (opens new window)。 这是一个封装print,它将输出限制在一个级别,避免被Android内核丢弃。

Flutter框架中的许多类都有toString实现。按照惯例,这些输出通常包括对象的runtimeType单行输出,通常在表单中ClassName(more information about this instance…)。 树中使用的一些类也具有toStringDeep,从该点返回整个子树的多行描述。已一些具有详细信息toString的类会实现一个toStringShort,它只返回对象的类型或其他非常简短的(一个或两个单词)描述

调试模式断言

在Flutter应用调试过程中,Dart assert语句被启用,并且 Flutter 框架使用它来执行许多运行时检查来验证是否违反一些不可变的规则。当一个某个规则被违反时,就会在控制台打印错误日志,并带上一些上下文信息来帮助追踪问题的根源

要关闭调试模式并使用发布模式,请使用flutter run --release运行我们的应用程序。 这也关闭了Observatory调试器。一个中间模式可以关闭除Observatory之外所有调试辅助工具的,称为“profile mode”,用--profile替代--release即可。

image.png
iPhone真机.jpg
断点

开发过程中,断点是最实用的调试工具之一,我们以 Android Studio 为例

调试应用程序层

Flutter框架的每一层都提供了将其当前状态或事件转储(dump)到控制台(使用debugPrint)的功能。

Widget 树

要转储Widgets树的状态,请调用debugDumpApp()。 只要应用程序已经构建了至少一次(即在调用build()之后的任何时间),我们可以在应用程序未处于构建阶段(即,不在build()方法内调用 )的任何时间调用此方法(在调用runApp()之后)。

如, 这个应用程序:

void main() {
  // runApp(const MyApp());
  runApp(MaterialApp(
    home: AppHome(),
  ));
}

class AppHome extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Material(
      child: Center(
        child: TextButton(
          onPressed: () {
            debugDumpApp();
          },
          child: Text('Dump App'),
        ),
      ),
    );
  }
}

…会输出这样的内容(精确的细节会根据框架的版本、设备的大小等等而变化):

flutter: WidgetsFlutterBinding - DEBUG MODE
flutter: [root](renderObject: RenderView#94f46)
flutter: └MaterialApp(state: _MaterialAppState#666fe)
flutter:  └ScrollConfiguration(behavior: MaterialScrollBehavior)
flutter:   └HeroControllerScope
flutter:    └Focus(state: _FocusState#b1685)
flutter:     └_FocusMarker
flutter:      └Semantics(container: false, properties: SemanticsProperties, renderObject: RenderSemanticsAnnotations#dbad1)
flutter:       └WidgetsApp-[GlobalObjectKey _MaterialAppState#666fe](state: _WidgetsAppState#8db01)
flutter:        └RootRestorationScope(state: _RootRestorationScopeState#4f8f2)
flutter:         └UnmanagedRestorationScope
flutter:          └RestorationScope(dependencies: [UnmanagedRestorationScope], state: _RestorationScopeState#96baa)
flutter:           └UnmanagedRestorationScope
flutter:            └SharedAppData(state: _SharedAppDataState#b7c55)
flutter:             └_SharedAppModel
flutter:              └Shortcuts(shortcuts: <Default WidgetsApp Shortcuts>, state: _ShortcutsState#ec884)
flutter:               └Focus(debugLabel: "Shortcuts", dependencies: [_FocusMarker], state: _FocusState#1d321)
flutter:                └_FocusMarker
... # 省略剩余内容

这是一个“扁平化”的树,显示了通过各种构建函数投影的所有widget(如果你在widget树的根中调用toStringDeepwidget,这是你获得的树)。 你会看到很多在你的应用源代码中没有出现的widget,因为它们是被框架中widget的build()函数插入的。例如,InkFeature (opens new window)是Material widget的一个实现细节

当按钮从被按下变为被释放时debugDumpApp()被调用,TextButton对象同时调用setState(),并将自己标记为"dirty"。 这就是为什么如果你看转储,你会看到特定的对象标记为“dirty”。我们还可以查看已注册了哪些手势监听器; 在这种情况下,一个单一的GestureDetector被列出,并且监听“tap”手势(“tap”是TapGestureDetector的toStringShort函数输出的)
如果我们编写自己的widget,则可以通过覆盖debugFillProperties() (opens new window)来添加信息。 将DiagnosticsProperty (opens new window)对象作为方法参数,并调用父类方法。 该函数是该toString方法用来填充小部件描述信息的

渲染树

如果我们尝试调试布局问题,那么Widget树可能不够详细。在这种情况下,我们可以通过调用debugDumpRenderTree()转储渲染树。 正如debugDumpApp(),除布局或绘制阶段外,我们可以随时调用此函数。作为一般规则,从frame 回调 (opens new window)或事件处理器中调用它是最佳解决方案。

要调用debugDumpRenderTree(),我们需要添加import'package:flutter/rendering.dart';到我们的源文件。

image.png

上面这个小例子的输出结果如下所示:

flutter: RenderView#ed02b
flutter:  │ debug mode enabled - iOS
flutter:  │ window size: Size(1170.0, 2532.0) (in physical pixels)
flutter:  │ device pixel ratio: 3.0 (physical pixels per logical pixel)
flutter:  │ configuration: Size(390.0, 844.0) at 3.0x (in logical pixels)
flutter:  │ semantics enabled
flutter:  │
flutter:  └─child: RenderSemanticsAnnotations#8cf5e
flutter:    │ needs compositing
flutter:    │ creator: Semantics ← _FocusMarker ← Focus ← HeroControllerScope ←
flutter:    │   ScrollConfiguration ← MaterialApp ← [root]
flutter:    │ parentData: <none>
flutter:    │ constraints: BoxConstraints(w=390.0, h=844.0)
flutter:    │ semantics node: SemanticsNode#1
flutter:    │ size: Size(390.0, 844.0)
...# 省略

这是根RenderObject对象的toStringDeep函数的输出。

当调试布局问题时,关键要看的是size和constraints字段。约束沿着树向下传递,尺寸向上传递。

如果我们编写自己的渲染对象,则可以通过覆盖debugFillProperties() (opens new window)将信息添加到转储。 将DiagnosticsProperty (opens new window)对象作为方法的参数,并调用父类方法

Layer树

读者可以理解为渲染树是可以分层的,而最终绘制需要将不同的层合成起来,而Layer则是绘制时需要合成的层,如果我们尝试调试合成问题,则可以使用debugDumpLayerTree() (opens new window)。对于上面的例子,它会输出:

Performing hot restart...
Syncing files to device iPhone 13...
Restarted application in 423ms.
flutter: TransformLayer#97e78
flutter:  │ owner: RenderView#b13a1
flutter:  │ creator: [root]
flutter:  │ engine layer: TransformEngineLayer#eaf8f
flutter:  │ handles: 1
flutter:  │ offset: Offset(0.0, 0.0)
flutter:  │ transform:
flutter:  │   [0] 3.0,0.0,0.0,0.0
flutter:  │   [1] 0.0,3.0,0.0,0.0
flutter:  │   [2] 0.0,0.0,1.0,0.0
flutter:  │   [3] 0.0,0.0,0.0,1.0
flutter:  │
flutter:  ├─child 1: OffsetLayer#e920c
flutter:  │ │ creator: RepaintBoundary ← FocusTrap ← _FocusMarker ← Semantics ←
flutter:  │ │   FocusScope ← PrimaryScrollController ← _ActionsMarker ← Actions
flutter:  │ │   ← Builder ← PageStorage ← Offstage ← _ModalScopeStatus ← ⋯
flutter:  │ │ engine layer: OffsetEngineLayer#1bc5c
flutter:  │ │ handles: 2
flutter:  │ │ offset: Offset(0.0, 0.0)
flutter:  │ │
flutter:  │ ├─child 1: PictureLayer#ab32f
flutter:  │ │   handles: 1
flutter:  │ │   paint bounds: Rect.fromLTRB(0.0, 0.0, 390.0, 844.0)
flutter:  │ │   picture: Picture#3a03a
flutter:  │ │   raster cache hints: isComplex = false, willChange = false
flutter:  │ │
flutter:  │ └─child 2: OffsetLayer#a82e6
flutter:  │   │ creator: RepaintBoundary-[GlobalKey#91cb3] ← IgnorePointer ←
flutter:  │   │   AnimatedBuilder ← Stack ←
flutter:  │   │   _CupertinoBackGestureDetector<dynamic> ← DecoratedBox ←
flutter:  │   │   DecoratedBoxTransition ← FractionalTranslation ←
flutter:  │   │   SlideTransition ← FractionalTranslation ← SlideTransition ←
flutter:  │   │   CupertinoPageTransition ← ⋯
flutter:  │   │ engine layer: OffsetEngineLayer#5a85c
flutter:  │   │ handles: 2
flutter:  │   │ offset: Offset(0.0, 0.0)
flutter:  │   │
flutter:  │   └─child 1: PhysicalModelLayer#4540c
flutter:  │     │ creator: PhysicalModel ← AnimatedPhysicalModel ← Material ←
flutter:  │     │   AppHome ← Semantics ← Builder ←
flutter:  │     │   RepaintBoundary-[GlobalKey#91cb3] ← IgnorePointer ←
flutter:  │     │   AnimatedBuilder ← Stack ←
flutter:  │     │   _CupertinoBackGestureDetector<dynamic> ← DecoratedBox ← ⋯
flutter:  │     │ engine layer: PhysicalShapeEngineLayer#8190b
flutter:  │     │ handles: 2
flutter:  │     │ elevation: 0.0
flutter:  │     │ color: Color(0xfffafafa)
flutter:  │     │
flutter:  │     └─child 1: PhysicalModelLayer#661e3
flutter:  │       │ creator: PhysicalShape ← _MaterialInterior ← Material ←
flutter:  │       │   ConstrainedBox ← _InputPadding ← Semantics ← TextButton ←
flutter:  │       │   Center ← DefaultTextStyle ← AnimatedDefaultTextStyle ←
flutter:  │       │   _InkFeatures-[GlobalKey#1c917 ink renderer] ←
flutter:  │       │   NotificationListener<LayoutChangedNotification> ← ⋯
flutter:  │       │ engine layer: PhysicalShapeEngineLayer#206cd
flutter:  │       │ handles: 2
flutter:  │       │ elevation: 0.0
flutter:  │       │ color: Color(0x00000000)
flutter:  │       │
flutter:  │       └─child 1: PictureLayer#d304e
flutter:  │           handles: 1
flutter:  │           paint bounds: Rect.fromLTRB(151.5, 404.0, 238.5, 440.0)
flutter:  │           picture: Picture#55495
flutter:  │           raster cache hints: isComplex = false, willChange = false
flutter:  │
flutter:  └─child 2: PictureLayer#90390
flutter:      handles: 1
flutter:      paint bounds: Rect.fromLTRB(0.0, 0.0, 1170.0, 2532.0)
flutter:      picture: Picture#c204c
flutter:      raster cache hints: isComplex = false, willChange = false
flutter:

这是根Layer的toStringDeep输出的。

根部的变换是应用设备像素比的变换; 在这种情况下,每个逻辑像素代表3.5个设备像素。

RepaintBoundary widget在渲染树的层中创建了一个RenderRepaintBoundary。这用于减少需要重绘的需求量

语义

我们还可以调用debugDumpSemanticsTree() (opens new window)获取语义树(呈现给系统可访问性API的树)的转储。 要使用此功能,必须首先启用辅助功能

调度

要找出相对于帧的开始/结束事件发生的位置,可以切换debugPrintBeginFrameBanner (opens new window)debugPrintEndFrameBanner (opens new window)布尔值以将帧的开始和结束打印到控制台。

debugPrintScheduleFrameStacks (opens new window)还可以用来打印导致当前帧被调度的调用堆栈

可视化调试

我们也可以通过设置debugPaintSizeEnabled为true以可视方式调试布局问题。 这是来自rendering库的布尔值。它可以在任何时候启用,并在为true时影响绘制。 设置它的最简单方法是在void main()的顶部设置。
当它被启用时,所有的盒子都会得到一个明亮的深青色边框,padding(来自widget如Padding)显示为浅蓝色,子widget周围有一个深蓝色框, 对齐方式(来自widget如Center和Align)显示为黄色箭头. 空白(如没有任何子节点的Container)以灰色显示。
debugPaintBaselinesEnabled (opens new window)做了类似的事情,但对于具有基线的对象,文字基线以绿色显示,表意(ideographic)基线以橙色显示

debugPaintPointersEnabled (opens new window)标志打开一个特殊模式,任何正在点击的对象都会以深青色突出显示。 这可以帮助我们确定某个对象是否以某种不正确的方式进行hit测试(Flutter检测点击的位置是否有能响应用户操作的widget),例如,如果它实际上超出了其父项的范围,首先不会考虑通过hit测试

如果我们尝试调试合成图层,例如以确定是否以及在何处添加RepaintBoundary widget,则可以使用debugPaintLayerBordersEnabled (opens new window)标志, 该标志用橙色或轮廓线标出每个层的边界,或者使用debugRepaintRainbowEnabled (opens new window)标志, 只要他们重绘时,这会使该层被一组旋转色所覆盖。

所有这些标志只能在调试模式下工作。通常,Flutter框架中以“debug...” 开头的任何内容都只能在调试模式下工作。

#

调试动画

调试动画最简单的方法是减慢它们的速度。为此,请将timeDilation (opens new window)变量(在scheduler库中)设置为大于1.0的数字,例如50.0。 最好在应用程序启动时只设置一次。如果我们在运行中更改它,尤其是在动画运行时将其值改小,则在观察时可能会出现倒退,这可能会导致断言命中,并且这通常会干扰我们的开发工作

调试性能问题

要了解我们的应用程序导致重新布局或重新绘制的原因,我们可以分别设置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目录下。输出列出了从应用程序启动到这些跟踪事件(以微秒捕获)所用的时间:

  • 进入Flutter引擎时.
  • 展示应用第一帧时.
  • 初始化Flutter框架时.
  • 完成Flutter框架初始化时.
image.png
image.png
跟踪Dart代码性能

要执行自定义性能跟踪和测量Dart任意代码段的wall/CPU时间(类似于在Android上使用systrace(opens new window))。 使用dart:developerTimeline (opens new window)工具来包含你想测试的代码块,例如:

Timeline.startSync('interesting function');
// iWonderHowLongThisTakes();
Timeline.finishSync();

然后打开你应用程序的Observatory timeline页面,在“Recorded Streams”中选择‘Dart’复选框,并执行你想测量的功能。
刷新页面将在Chrome的跟踪工具 (opens new window)中显示应用按时间顺序排列的timeline记录。

请确保运行flutter run时带有--profile标志,以确保运行时性能特征与我们的最终产品差异最小。

DevTools

Flutter DevTools 是 Flutter 可视化调试工具,如下图。它将各种调试工具和能力集成在一起,并提供可视化调试界面,它的功能很强大,掌握它会对我们开发和优化 Flutter 应用有很大帮助。由于 Flutter DevTools 功能很多,短篇幅是讲不完的,本书不做专门介绍,Flutter 官网对 DevTools 有详细的介绍,读者可以去官网查看相关教程。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容