阅读(2168) (0)

Flutter实战 调试Flutter应用

2021-03-06 15:26:05 更新

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

#Dart 分析器

在运行应用程序前,请运行flutter analyze测试你的代码。这个工具是一个静态代码检查工具,它是dartanalyzer工具的一个包装,主要用于分析代码并帮助开发者发现可能的错误,比如,Dart 分析器大量使用了代码中的类型注释来帮助追踪问题,避免var、无类型的参数、无类型的列表文字等。

如果你使用 IntelliJ 的 Flutter插件,那么分析器在打开 IDE 时就已经自动启用了,如果读者使用的是其它 IDE,强烈建议读者启用 Dart 分析器,因为在大多数时候,Dart 分析器可以在代码运行前发现大多数问题。

#Dart Observatory (语句级的单步调试和分析器)

如果我们使用flutter run启动应用程序,那么当它运行时,我们可以打开 Observatory 工具的 Web 页面,例如 Observatory 默认监听http://127.0.0.1:8100/ (opens new window),可以在浏览器中直接打开该链接。直接使用语句级单步调试器连接到您的应用程序。如果您使用的是 IntelliJ,则还可以使用其内置的调试器来调试您的应用程序。

Observatory 同时支持分析、检查堆等。有关 Observatory 的更多信息请参考Observatory 文档 (opens new window)。

如果您使用 Observatory 进行分析,请确保通过--profile选项来运行flutter run命令来运行应用程序。 否则,配置文件中将出现的主要问题将是调试断言,以验证框架的各种不变量(请参阅下面的“调试模式断言”)。

#debugger() 声明

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

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

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

#printdebugPrintflutter 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即可。

#调试应用程序层

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

#Widget 树

要转储 Widgets 树的状态,请调用debugDumpApp() (opens new window)。 只要应用程序已经构建了至少一次(即在调用build()之后的任何时间),您可以在应用程序未处于构建阶段(即,不在build()方法内调用 )的任何时间调用此方法(在调用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'),
        ),
      ),
    );
  }
}

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

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))
... #省略剩余内容

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

当按钮从被按下变为被释放时 debugDumpApp() 被调用,FlatButton 对象同时调用setState(),并将自己标记为"dirty"。 这就是为什么如果你看转储,你会看到特定的对象标记为“dirty”。您还可以查看已注册了哪些手势监听器; 在这种情况下,一个单一的 GestureDetector 被列出,并且监听“tap”手势(“tap”是TapGestureDetectortoStringShort函数输出的)

如果您编写自己的 widget,则可以通过覆盖debugFillProperties() (opens new window)来添加信息。 将 DiagnosticsProperty (opens new window)对象作为方法参数,并调用父类方法。 该函数是该toString方法用来填充小部件描述信息的。

#渲染树

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

要调用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函数的输出。

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

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

#Layer树

读者可以理解为渲染树是可以分层的,而最终绘制需要将不同的层合成起来,而 Layer 则是绘制时需要合成的层,如果您尝试调试合成问题,则可以使用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 在渲染树的层中创建了一个RenderRepaintBoundary。这用于减少需要重绘的需求量。

#语义

您还可以调用debugDumpSemanticsTree() (opens new window)获取语义树(呈现给系统可访问性 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")

#调度

要找出相对于帧的开始/结束事件发生的位置,可以切换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)还可以用来打印导致当前帧被调度的调用堆栈。

#可视化调试

您也可以通过设置debugPaintSizeEnabledtrue以可视方式调试布局问题。 这是来自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-startupprofile选项。

$ flutter run --trace-startup --profile

跟踪输出保存为start_up_info.json,在 Flutter 工程目录在 build 目录下。输出列出了从应用程序启动到这些跟踪事件(以微秒捕获)所用的时间:

  • 进入 Flutter 引擎时.
  • 展示应用第一帧时.
  • 初始化 Flutter 框架时.
  • 完成 Flutter 框架初始化时.

如 :

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

#跟踪Dart代码性能

要执行自定义性能跟踪和测量 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’复选框,并执行你想测量的功能。

刷新页面将在Chrome的跟踪工具 (opens new window)中显示应用按时间顺序排列的 timeline 记录。

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