初略讲解如何调试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应用程序上覆盖这样的网格。

相关文章
相关标签/搜索