Flutter App 软件调试指南 | 开发者说·DTalk

2019 年 11 月 23 日 谷歌开发者
本文原作者: 萧文翰,原文发布于 GitChat: https://gitbook.cn/books/5d3dbd33213ff5074401339c/index.html


在实际开发中,测试和调试所占的时间比例,在总开发时间中还是比较高的。在修复产品缺陷时,我们通常需要实时观察某个对象的值。虽然可以通过 Log 的形式进行输出,但在某些情形下,使用更好的调试工具可以使观察这些值变得更加方便。 


想象一下,如果需要观察一个集合,或者一个对象中所有变量的值,单纯地使用 Log 需要怎么做?

可能会想到用循环,也可能会在输出 Log 的代码中多次运用 "." 运算符对对象内的变量取值。这使得编写 Log 输出语句本身变得复杂,再加上可能还会冒着空指针的风险。 

本文涵盖了 Flutter App 代码的所有调试方式,通过本场 Chat 的学习,您将会得到以下知识: 
  • 认识 Dart 语言检查器
  • 如何在 IDE 中进行单步调试
  • 打印 Log 的技巧
  • 利用 Dart 语言中的 "断言"
  • 如何查看界面 Widget 树形层级
  • 怎样获取语义树
下面我们来逐一进行学习。


认识 Dart 语言检查器


在运行应用程序前,使用 Dart 语言检查器,通过分析代码,可以帮助开发者排除一些代码隐患。 当然,如果读者使用的是 Android Studio,Dart 检查器在默认情况下会自动启用。 若要手动测试代码,可以在工程根目录下执行:
flutter analyze


命令,检查结果会稍后显示在命令行对话框中。 比如,在默认新建的计数器应用中,去掉一个语句结尾的分号:

void _incrementCounter() {  setState(() {    _counter++  });}

看到 _counter++ 后面少了一个分号了吗?此时,运行 Dart 检查器,命令行输出:

error - Expected to find ';' - lib\main.dart:32:15 - expected_token1 issue found. (ran in 8.9s)


如何在 IDE 中进行单步调试


在某些时候,我们需要进行单步调试。单步调试可以让程序逐条语句地进行,并可以看到当前运行的位置。另外,在单步调试过程中,还能实时关注相应范围内所有变量值的详细变化过程。


Android Studio 中提供了单步调试功能。这和开发原生 Android 平台 App 时的单步调试方法一样,其具体步骤可以分为三步进行,第一步是标记断点,第二步是运行程序到断点处,第三步则是使用 Debug 工具进行调试。


下面以默认的计数器应用为例,观察代码中 _counter 值的变化,体会单步调试的全过程。


第一步是标记断点,既然要观察 _counter 值的变化,则在每次 _counter 值发生变化后添加断点,观察数值变化是最理想的,因此在行号稍右侧点击鼠标,把断点加载下图所示的位置。

添加断点后,相应的行号右侧将会出现圆形的断点标记,并且整行将会高亮显示。 


到此,断点就添加好了,当然,还可以同时添加多个断点,以便实现多个位置的调试。 


接下来则是运行程序。和之前的运行方式不同,这一次需要以调试模式启动 App。方法是点击 Android Studio 上方工具栏的小虫子图标,如下图所示:

稍等片刻,程序就启动了。由于我们添加断点的位置在程序启动后会被立即运行到,因此,无需其他操作,即可进入调试视图。如果断点位置并不是在程序一启动就执行,则需要手动让程序运行到断点位置。

下图展示了代码运行到断点位置时的 IDE 视图,它自动进入了 Debug 视图模式:

这里介绍两种方法来获取 _counter 的值,一种是在代码处,通过执行表达式的方式,如下图所示:

在相应的变量上点右键,接着在弹出的菜单中选择计算表达式 (Evaluate Expression),最后在弹出的对话框中点击 Evaluate 按钮,得到运算结果如下图所示:
除此之外,如果所取得值是一个对象,还可以在该窗口上方的表达式输入框中使用 "." 运算符调用该对象的方法,获取相应的运算结果。另外一种方式则是通过窗口下方的调试视图,在这里会有较为完整的变量值显示,它们以树形的方式呈现。我们可以依次展开这个树形结构,找到 _counter 值,如下图所示:
接下来,保持 App 继续运行,然后点击界面右下角的 FloatingActionButton,验证一下点击后 _counter 值的准确性。此时,需要点击 "运行到下一个断点处" 按钮,如下图所示:
点击该按钮后,程序会继续运行,直到下一个断点处。

本例只添加了一个断点,因此,程序再次停留在此处。如下图所示,点击 FloatingActionButton 后,程序再次停留在断点位置,此时,_counter 值已经发生了变化,完成了自增 1 的计算,结果无误。

我们可以在任何时候退出调试模式,只需点击停止运行按钮即可,它位于启动调试模式按钮的右侧。

点击该按钮后,Android Studio 会退出调试模式,运行在设备上的程序也会被强制关闭。



打印 Log 的技巧


为了跟踪和记录软件的运行情况,开发者们通常会输出一些日志 (Log),这些日志对于用户而言是不可见的。传统的 iOS 和 Android 平台都提供了完善的日志输出功能,Flutter 也不例外。要实时查看 Flutter 的日志,只需在控制台中输入:

flutter logs


即可。Android Studio 和 Visual Studio Code 中,默认集成了控制台 (console),使用这个集成的控制台或者启动一个新的控制台皆可。这里要注意的是,一旦执行了上面的命令,该控制台将会进入独占状态,即无法再使用其他的命令了,除非中断 Log 查看。 


当我们想要在程序运行得某个地方输出 Log 时,通常使用 debugPrint() 方法。结合之前的示例,修改原先的 main() 方法,添加一个 Log 输出,内容为 "我开始启动了",未经修改的代码:

void main() => runApp(MyApp());


添加 Log 后的代码:

void main() { debugPrint("我开始启动了"); runApp(MyApp()); }


在控制台中使用 flutter logs 命令监视 Log 输出,然后重新安装并运行程序,控制台输出:

I/flutter (12705): 我开始启动了


结果如图所示:

要结束监视 Log 输出,可使用 Control + C 组合键,然后输入 y,回车确认,也可直接关闭控制台。最后,需要注意的是,为了保证 Log 输出正确无误,建议各位读者使用英文输出,而不是直接使用中文。因为在某些情况下,可能会导致显示乱码。经测试,在英文版的 Windows 下启动命令提示符,并执行上例,会得到如下输出:
I/flutter (13320): 我开始启动了



利用 Dart 语言中的 "断言"


Dart 运行时提供两种运行方式: Production 和 Checked。默认情况下会以 Production 模式运行,在这种条件下,优先考虑性能,关闭类型检查和断言;反之,Checked 模式更利于开发阶段调试使用。


断言可以检查程序中某些可能出现的运行时逻辑错误。比如下面这段代码:

// assertvar intValue = 300;assert(intValue == 299);


很明显,intValue 不满足和 299 相等的条件,此时在开发环境中运行程序,将看到控制台报错。而一旦切换到生产模式,则不会收到任何错误提示。这对于检查代码中某些隐含的逻辑问题十分有效。



如何查看界面 Widget 树形层级


Flutter 框架中的每一层都提供了转储当前状态或事件的方式,这些转储的信息将通过 debugPrint() 方式输出到控制台上。

下面就让我们逐层探究,了解其 Log 日志转储的方法和内容。


组件层

要转储组件层的状态,需要调用 debugdumpapp() 方法。

保证该方法取到有效 Log 的前提是要确保该 App 至少构建了一次组件,且不要在 build() 过程中调用。 我们以新建的计数器应用为例,添加一个创建组件层日志转储的按钮,并在用户点击该按钮后,执行 debugdumpapp() 方法。 具体实现如下:
import 'package:flutter/material.dart';void main() => runApp(MyApp());class MyApp extends StatelessWidget {  @override  Widget build(BuildContext context) {    return MaterialApp(      title: 'Flutter Demo',      theme: ThemeData(        primarySwatch: Colors.blue,      ),      home: MyHomePage(title: 'Flutter Demo Home Page'),    );  }}class MyHomePage extends StatefulWidget {  MyHomePage({Key key, this.title}) : super(key: key);  final String title;  @override  _MyHomePageState createState() => _MyHomePageState();}class _MyHomePageState extends State<MyHomePage> {  int _counter = 0;  void _incrementCounter() {    setState(() {      _counter++;    });  }  @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(        title: Text(widget.title),      ),      body: Center(        child: Column(          mainAxisAlignment: MainAxisAlignment.center,          children: <Widget>[            Text(              'You have pushed the button this many times:',            ),            Text(              '$_counter',              style: Theme.of(context).textTheme.display1,            ),            RaisedButton(                onPressed: () => debugDumpApp(),                child: Text("Create app dump")),          ],        ),      ),      floatingActionButton: FloatingActionButton(        onPressed: _incrementCounter,        tooltip: 'Increment',        child: Icon(Icons.add),      ),    );  }}

注意新添加的 RaisedButton。运行上述代码,然后点击该按钮,可以看到控制台如下输出 (节选):
I/flutter ( 4489): WidgetsFlutterBinding - CHECKED MODEI/flutter ( 4489): [root](renderObject: RenderView#d27b1)I/flutter ( 4489): └MyAppI/flutter ( 4489):  └MaterialApp(state: _MaterialAppState#51668)I/flutter ( 4489):   └ScrollConfiguration(behavior: _MaterialScrollBehavior)I/flutter ( 4489):    └WidgetsApp-[GlobalObjectKey _MaterialAppState#51668](state: _WidgetsAppState#04e30)I/flutter ( 4489):     └MediaQuery(MediaQueryData(size: Size(411.4, 797.7), devicePixelRatio: 2.6, textScaleFactor: 1.1, platformBrightness: Brightness.light, padding: EdgeInsets(0.0, 24.0, 0.0, 0.0), viewInsets: EdgeInsets.zero, alwaysUse24HourFormat: true, accessibleNavigation: falsedisableAnimations: falseinvertColors: falseboldText: false))I/flutter ( 4489):      └Localizations(locale: en_US, delegates: [DefaultMaterialLocalizations.delegate(en_US), DefaultCupertinoLocalizations.delegate(en_US), DefaultWidgetsLocalizations.delegate(en_US)], state: _LocalizationsState#c0c98)I/flutter ( 4489):       └Semantics(container: false, properties: SemanticsProperties, label: null, value: null, hint: null, textDirection: ltr, hintOverrides: null, renderObject: RenderSemanticsAnnotations#31c77)I/flutter ( 4489):        └_LocalizationsScope-[GlobalKey#60b05]I/flutter ( 4489):         └Directionality(textDirection: ltr)I/flutter ( 4489):          └Title(title: "Flutter Demo", color: MaterialColor(primary value: Color(0xff2196f3)))I/flutter ( 4489):           └CheckedModeBanner("DEBUG")I/flutter ( 4489):            └Banner("DEBUG", textDirection: ltr, location: topEnd, Color(0xa0b71c1c), text inherit: true, text color: Color(0xffffffff), text size: 10.2, text weight: 900, text height: 1.0x, dependencies: [Directionality])I/flutter ( 4489):             └CustomPaint(renderObject: RenderCustomPaint#c2a34)I/flutter ( 4489):              └DefaultTextStyle(debugLabel: fallback style; consider putting your text in a Material, inherit: true, color: Color(0xd0ff0000), family: monospace, size: 48.0, weight: 900, decoration: double Color(0xffffff00) TextDecoration.underline, softWrap: wrapping at box width, overflow: clip)I/flutter ( 4489):               └Builder(dependencies: [MediaQuery])……

实际的输出内容要比上述节选部分多好几倍。

这些内容乍看上去似乎很复杂的样子,但仔细观察后发现: 组件层的转储信息实际上就是把所有的组件按照树形结构罗列了出来。其中包含了组件的样式、值等信息。

当然,还会看到某些未曾在代码中体现的组件。这是因为这些组件在框架本身的组件中有所使用。比如 RaisedButton 中的 InkWell,虽然没有通过代码实现 InkWell,但 RaisedButton 本身为了实现相应的效果,在其中使用了 InkWell 组件。

此外,在转储信息中,会有某个组件被标记为 dirty,这是因为创建转储信息的行为是通过该组件触发的。本例中,被标记为 dirty 的组件如下:
RaisedButton(dependencies: [_LocalizationsScope-[GlobalKey#60b05], _InheritedTheme])    └RawMaterialButton(dirty, state: _RawMaterialButtonState#fe2da)


可见,它就是为了执行 debugDumpApp() 方法而增加的按钮。


渲染层

由上一小节得知组件层提供了各个组件的详情信息。但某些时候,这些信息并不完全够使用,此时可以调用 debugDumpRenderTree() 方法转储渲染层。


基于上小节的示例,继续添加一个按钮,其操作就是触发 debugDumpRenderTree() 方法。如下:

RaisedButton(    onPressed: () => debugDumpRenderTree(),    child: Text("Create render tree dump"))


程序运行后,单击这个按钮,观察控制台输出 (节选):

I/flutter ( 7255): RenderView#7e860I/flutter ( 7255):  │ debug mode enabled - androidI/flutter ( 7255):  │ window size: Size(1080.0, 2094.0) (in physical pixels)I/flutter ( 7255):  │ device pixel ratio: 2.6 (physical pixels per logical pixel)I/flutter ( 7255):  │ configuration: Size(411.4, 797.7) at 2.625x (in logical pixels)I/flutter ( 7255):  │I/flutter ( 7255):  └─child: RenderSemanticsAnnotations#62d7dI/flutter ( 7255):    │ creator: Semantics ← Localizations ← MediaQuery ←I/flutter ( 7255):    │   WidgetsApp-[GlobalObjectKey _MaterialAppState#d0498] ←I/flutter ( 7255):    │   ScrollConfiguration ← MaterialApp ← MyApp ← [root]I/flutter ( 7255):    │ parentData: <none>I/flutter ( 7255):    │ constraints: BoxConstraints(w=411.4, h=797.7)I/flutter ( 7255):    │ size: Size(411.4, 797.7)I/flutter ( 7255):    │I/flutter ( 7255):    └─child: RenderCustomPaint#e2d03I/flutter ( 7255):      │ creator: CustomPaint ← Banner ← CheckedModeBanner ← Title ←I/flutter ( 7255):      │   Directionality ← _LocalizationsScope-[GlobalKey#6be84] ←I/flutter ( 7255):      │   Semantics ← Localizations ← MediaQuery ←I/flutter ( 7255):      │   WidgetsApp-[GlobalObjectKey _MaterialAppState#d0498] ←I/flutter ( 7255):      │   ScrollConfiguration ← MaterialApp ← ⋯I/flutter ( 7255):      │ parentData: <none> (can use size)I/flutter ( 7255):      │ constraints: BoxConstraints(w=411.4, h=797.7)I/flutter ( 7255):      │ size: Size(411.4, 797.7)I/flutter ( 7255):      │I/flutter ( 7255):      └─child: RenderPointerListener#9b873I/flutter ( 7255):        │ creator: Listener ← Navigator-[GlobalObjectKey<NavigatorState>I/flutter ( 7255):        │   _WidgetsAppState#74612] ← IconTheme ← IconTheme ←I/flutter ( 7255):        │   _InheritedCupertinoTheme ← CupertinoTheme ← _InheritedTheme ←I/flutter ( 7255):        │   Theme ← AnimatedTheme ← Builder ← DefaultTextStyle ←I/flutter ( 7255):        │   CustomPaint ← ⋯I/flutter ( 7255):        │ parentData: <none> (can use size)I/flutter ( 7255):        │ constraints: BoxConstraints(w=411.4, h=797.7)I/flutter ( 7255):        │ size: Size(411.4, 797.7)I/flutter ( 7255):        │ behavior: deferToChildI/flutter ( 7255):        │ listeners: down, up, cancelI/flutter ( 7255):        │I/flutter ( 7255):        └─child: RenderAbsorbPointer#52153I/flutter ( 7255):          │ creator: AbsorbPointer ← Listener ←……


这段节选依然比实际输出少很多。


不过,这些转储信息,通常只关注 size 和 constrains 参数就可以了。因为它们表示了大小和约束条件。此外,针对盒约束,还有可能存在 relayoutSubtreeRoot,它表示有多少父控件依赖该组件的尺寸。


层的合成

如果要调试有关合成的问题,就需要转储层级关系的信息。转储层级关系的方法是 debugDumpLayerTree()。我们继续添加一个按钮,其操作就是触发 debugDumpLayerTree() 方法。如下:

RaisedButton(    onPressed: () => debugDumpLayerTree(),    child: Text("Create layer tree dump"))


运行,并点击该按钮,得到控制台输出:
I/flutter (10050): TransformLayer#256a4I/flutter (10050):  │ owner: RenderView#6c917I/flutter (10050):  │ creator: [root]I/flutter (10050):  │ offset: Offset(0.0, 0.0)I/flutter (10050):  │ transform:I/flutter (10050):  │   [0] 2.625,0.0,0.0,0.0I/flutter (10050):  │   [1] 0.0,2.625,0.0,0.0I/flutter (10050):  │   [2] 0.0,0.0,1.0,0.0I/flutter (10050):  │   [3] 0.0,0.0,0.0,1.0I/flutter (10050):  │I/flutter (10050):  ├─child 1: OffsetLayer#03fdcI/flutter (10050):  │ │ creator: RepaintBoundary ← _FocusScopeMarker ← Semantics ←I/flutter (10050):  │ │   FocusScope ← PageStorage ← Offstage ← _ModalScopeStatus ←I/flutter (10050):  │ │   _ModalScope<dynamic>-[LabeledGlobalKey<_ModalScopeState<dynamic>>#ea0b8]I/flutter (10050):  │ │   ← _OverlayEntry-[LabeledGlobalKey<_OverlayEntryState>#d7b44] ←I/flutter (10050):  │ │   Stack ← _Theatre ←I/flutter (10050):  │ │   Overlay-[LabeledGlobalKey<OverlayState>#404b4] ← ⋯I/flutter (10050):  │ │ offset: Offset(0.0, 0.0)I/flutter (10050):  │ │I/flutter (10050):  │ └─child 1: OffsetLayer#9ca96I/flutter (10050):  │   │ creator: RepaintBoundary-[GlobalKey#71e3e] ← IgnorePointer ←I/flutter (10050):  │   │   FadeTransition ← FractionalTranslation ← SlideTransition ←I/flutter (10050):  │   │   _FadeUpwardsPageTransition ← AnimatedBuilder ← RepaintBoundaryI/flutter (10050):  │   │   ← _FocusScopeMarker ← Semantics ← FocusScope ← PageStorage ← ⋯I/flutter (10050):  │   │ offset: Offset(0.0, 0.0)I/flutter (10050):  │   │I/flutter (10050):  │   └─child 1: PhysicalModelLayer#9986eI/flutter (10050):  │     │ creator: PhysicalModel ← AnimatedPhysicalModel ← Material ←I/flutter (10050):  │     │   PrimaryScrollController ← _ScaffoldScope ← Scaffold ←I/flutter (10050):  │     │   MyHomePage ← Semantics ← Builder ←I/flutter (10050):  │     │   RepaintBoundary-[GlobalKey#71e3e] ← IgnorePointer ←I/flutter (10050):  │     │   FadeTransition ← ⋯I/flutter (10050):  │     │ elevation: 0.0I/flutter (10050):  │     │ color: Color(0xfffafafa)I/flutter (10050):  │     │I/flutter (10050):  │     ├─child 1: PictureLayer#1f44bI/flutter (10050):  │     │   paint bounds: Rect.fromLTRB(0.0, 0.0, 411.4, 797.7)I/flutter (10050):  │     │I/flutter (10050):  │     ├─child 2: PhysicalModelLayer#e486cI/flutter (10050):  │     │ │ creator: PhysicalShape ← _MaterialInterior ← Material ←I/flutter (10050):  │     │ │   ConstrainedBox ← _InputPadding ← Semantics ← RawMaterialButtonI/flutter (10050):  │     │ │   ← RaisedButton ← Column ← Center ← MediaQuery ←I/flutter (10050):  │     │ │   LayoutId-[<_ScaffoldSlot.body>] ← ⋯I/flutter (10050):  │     │ │ elevation: 2.0I/flutter (10050):  │     │ │ color: Color(0xffe0e0e0)I/flutter (10050):  │     │ │I/flutter (10050):  │     │ └─child 1: PictureLayer#225deI/flutter (10050):  │     │     paint bounds: Rect.fromLTRB(130.2, 403.9, 281.2, 439.9)I/flutter (10050):  │     │I/flutter (10050):  │     ├─child 3: PhysicalModelLayer#f4d9aI/flutter (10050):  │     │ │ creator: PhysicalShape ← _MaterialInterior ← Material ←I/flutter (10050):  │     │ │   ConstrainedBox ← _InputPadding ← Semantics ← RawMaterialButtonI/flutter (10050):  │     │ │   ← RaisedButton ← Column ← Center ← MediaQuery ←I/flutter (10050):  │     │ │   LayoutId-[<_ScaffoldSlot.body>] ← ⋯I/flutter (10050):  │     │ │ elevation: 2.0I/flutter (10050):  │     │ │ color: Color(0xffe0e0e0)I/flutter (10050):  │     │ │I/flutter (10050):  │     │ └─child 1: PictureLayer#c7bafI/flutter (10050):  │     │     paint bounds: Rect.fromLTRB(105.2, 451.9, 306.2, 487.9)I/flutter (10050):  │     │I/flutter (10050):  │     ├─child 4: PhysicalModelLayer#eb57bI/flutter (10050):  │     │ │ creator: PhysicalShape ← _MaterialInterior ← Material ←I/flutter (10050):  │     │ │   ConstrainedBox ← _InputPadding ← Semantics ← RawMaterialButtonI/flutter (10050):  │     │ │   ← RaisedButton ← Column ← Center ← MediaQuery ←I/flutter (10050):  │     │ │   LayoutId-[<_ScaffoldSlot.body>] ← ⋯I/flutter (10050):  │     │ │ elevation: 2.0I/flutter (10050):  │     │ │ color: Color(0xffe0e0e0)I/flutter (10050):  │     │ │I/flutter (10050):  │     │ └─child 1: PictureLayer#2350dI/flutter (10050):  │     │     paint bounds: Rect.fromLTRB(111.2, 499.9, 300.2, 535.9)I/flutter (10050):  │     │I/flutter (10050):  │     ├─child 5: AnnotatedRegionLayer<SystemUiOverlayStyle>#a5e42I/flutter (10050):  │     │ │ value: {systemNavigationBarColor: 4278190080,I/flutter (10050):  │     │ │   systemNavigationBarDividerColor: null, statusBarColor: null,I/flutter (10050):  │     │ │   statusBarBrightness: Brightness.dark, statusBarIconBrightness:I/flutter (10050):  │     │ │   Brightness.light, systemNavigationBarIconBrightness:I/flutter (10050):  │     │ │   Brightness.light}I/flutter (10050):  │     │ │ size: Size(411.4, 80.0)I/flutter (10050):  │     │ │ offset: Offset(0.0, 0.0)I/flutter (10050):  │     │ │I/flutter (10050):  │     │ └─child 1: PhysicalModelLayer#32968I/flutter (10050):  │     │   │ creator: PhysicalModel ← AnimatedPhysicalModel ← Material ←I/flutter (10050):  │     │   │   AnnotatedRegion<SystemUiOverlayStyle> ← Semantics ← AppBar ←I/flutter (10050):  │     │   │   FlexibleSpaceBarSettings ← ConstrainedBox ← MediaQuery ←I/flutter (10050):  │     │   │   LayoutId-[<_ScaffoldSlot.appBar>] ← CustomMultiChildLayout ←I/flutter (10050):  │     │   │   AnimatedBuilder ← ⋯I/flutter (10050):  │     │   │ elevation: 4.0I/flutter (10050):  │     │   │ color: MaterialColor(primary value: Color(0xff2196f3))I/flutter (10050):  │     │   │I/flutter (10050):  │     │   └─child 1: PictureLayer#e562bI/flutter (10050):  │     │       paint bounds: Rect.fromLTRB(0.0, 0.0, 411.4, 80.0)I/flutter (10050):  │     │I/flutter (10050):  │     └─child 6: TransformLayer#4e3f3I/flutter (10050):  │       │ offset: Offset(0.0, 0.0)I/flutter (10050):  │       │ transform:I/flutter (10050):  │       │   [0] 1.0,2.4492935982947064e-16,0.0,-1.7053025658242404e-13I/flutter (10050):  │       │   [1] -2.4492935982947064e-16,1.0,0.0,1.1368683772161603e-13I/flutter (10050):  │       │   [2] 0.0,0.0,1.0,0.0I/flutter (10050):  │       │   [3] 0.0,0.0,0.0,1.0I/flutter (10050):  │       │I/flutter (10050):  │       └─child 1: PhysicalModelLayer#79c2cI/flutter (10050):  │         │ creator: PhysicalShape ← _MaterialInterior ← Material ←I/flutter (10050):  │         │   ConstrainedBox ← _InputPadding ← Semantics ← RawMaterialButtonI/flutter (10050):  │         │   ← Semantics ← Listener ← RawGestureDetector ← GestureDetector ←I/flutter (10050):  │         │   Tooltip ← ⋯I/flutter (10050):  │         │ elevation: 6.0I/flutter (10050):  │         │ color: Color(0xff2196f3)I/flutter (10050):  │         │I/flutter (10050):  │         └─child 1: PictureLayer#0e8dcI/flutter (10050):  │             paint bounds: Rect.fromLTRB(339.4, 725.7, 395.4, 781.7)I/flutter (10050):  │I/flutter (10050):  └─child 2: PictureLayer#1ae80I/flutter (10050):      paint bounds: Rect.fromLTRB(0.0, 0.0, 1080.0, 2094.0)I/flutter (10050):

这是完整的输出。由于界面层级之间的结合非常简单,因此这部分的 Log 会较短。上面的 Log 中,RepaintBoundary 组件在渲染树中创建 RenderRepaintBoundary,从而在层级的树形结构中创建一个新层。这一步骤用来减少重新绘图的需求。



怎样获取语义树


语义调试通常用于提供系统辅助功能的 App 中,当系统辅助功能开启时,系统会根据 App 提供的语义理解某个组件是做什么用的,或简单地表明组件的内容。 调试语义实际上就是输出 "语义树"。 和前两小节不同,要获得语义树,首先要在一开始做声明,如下所示:
class MyApp extends StatelessWidget {  @override  Widget build(BuildContext context) {    return MaterialApp(      showSemanticsDebugger: true,      title: 'Flutter Demo',      theme: ThemeData(        primarySwatch: Colors.blue,      ),      home: MyHomePage(title: 'Flutter Demo Home Page'),    );  }}


特别留意上述代码中的 showSemanticsDebugger,它就是开启语义调试的前提。接下来就是添加语义树输出的方法了,这一步骤和前两小节类似,如下所示:
RaisedButton(    onPressed: () => debugDumpSemanticsTree(DebugSemanticsDumpOrder.traversalOrder),    child: Text("Create semantics tree dump"))

此时,运行 App,得到如下图所示的界面,则表示配置成功。

此时,我们单击调试语义按钮,观察控制台的输出:
I/flutter ( 8341): SemanticsNode#0I/flutter ( 8341):  │ Rect.fromLTRB(0.0, 0.0, 1080.0, 1794.0)I/flutter ( 8341):  │I/flutter ( 8341):  └─SemanticsNode#1I/flutter ( 8341):    │ Rect.fromLTRB(0.0, 0.0, 411.4, 683.4) scaled by 2.6xI/flutter ( 8341):    │ textDirection: ltrI/flutter ( 8341):    │I/flutter ( 8341):    └─SemanticsNode#2I/flutter ( 8341):      │ Rect.fromLTRB(0.0, 0.0, 411.4, 683.4)I/flutter ( 8341):      │ flags: scopesRouteI/flutter ( 8341):      │I/flutter ( 8341):      ├─SemanticsNode#9I/flutter ( 8341):      │ │ Rect.fromLTRB(0.0, 0.0, 411.4, 80.0)I/flutter ( 8341):      │ │ thicknes: 4.0I/flutter ( 8341):      │ │I/flutter ( 8341):      │ └─SemanticsNode#10I/flutter ( 8341):      │     Rect.fromLTRB(16.0, 40.5, 242.0, 63.5)I/flutter ( 8341):      │     flags: isHeader, namesRouteI/flutter ( 8341):      │     label: "Flutter Demo Home Page"I/flutter ( 8341):      │     textDirection: ltrI/flutter ( 8341):      │     elevation: 4.0I/flutter ( 8341):      │I/flutter ( 8341):      ├─SemanticsNode#3I/flutter ( 8341):      │   Rect.fromLTRB(65.7, 257.7, 345.7, 273.7)I/flutter ( 8341):      │   label: "You have pushed the button this many times:"I/flutter ( 8341):      │   textDirection: ltrI/flutter ( 8341):      │I/flutter ( 8341):      ├─SemanticsNode#4I/flutter ( 8341):      │   Rect.fromLTRB(195.7, 273.7, 215.7, 313.7)I/flutter ( 8341):      │   label: "0"I/flutter ( 8341):      │   textDirection: ltrI/flutter ( 8341):      │I/flutter ( 8341):      ├─SemanticsNode#5I/flutter ( 8341):      │   Rect.fromLTRB(135.7, 313.7, 275.7, 361.7)I/flutter ( 8341):      │   actions: tapI/flutter ( 8341):      │   flags: isButton, hasEnabledState, isEnabledI/flutter ( 8341):      │   label: "Create app dump"I/flutter ( 8341):      │   textDirection: ltrI/flutter ( 8341):      │   thicknes: 2.0I/flutter ( 8341):      │I/flutter ( 8341):      ├─SemanticsNode#6I/flutter ( 8341):      │   Rect.fromLTRB(113.2, 361.7, 298.2, 409.7)I/flutter ( 8341):      │   actions: tapI/flutter ( 8341):      │   flags: isButton, hasEnabledState, isEnabledI/flutter ( 8341):      │   label: "Create render tree dump"I/flutter ( 8341):      │   textDirection: ltrI/flutter ( 8341):      │   thicknes: 2.0I/flutter ( 8341):      │I/flutter ( 8341):      ├─SemanticsNode#7I/flutter ( 8341):      │   Rect.fromLTRB(118.2, 409.7, 293.2, 457.7)I/flutter ( 8341):      │   actions: tapI/flutter ( 8341):      │   flags: isButton, hasEnabledState, isEnabledI/flutter ( 8341):      │   label: "Create layer tree dump"I/flutter ( 8341):      │   textDirection: ltrI/flutter ( 8341):      │   thicknes: 2.0I/flutter ( 8341):      │I/flutter ( 8341):      ├─SemanticsNode#8I/flutter ( 8341):      │   Rect.fromLTRB(100.7, 457.7, 310.7, 505.7)I/flutter ( 8341):      │   actions: tapI/flutter ( 8341):      │   flags: isButton, hasEnabledState, isEnabledI/flutter ( 8341):      │   label: "Create semantics tree dump"I/flutter ( 8341):      │   textDirection: ltrI/flutter ( 8341):      │   thicknes: 2.0I/flutter ( 8341):      │I/flutter ( 8341):      └─SemanticsNode#11I/flutter ( 8341):        │ merge boundary ⛔️I/flutter ( 8341):        │ Rect.fromLTRB(0.0, 0.0, 56.0, 56.0) with transformI/flutter ( 8341):        │   [1.0,2.4492935982947064e-16,0.0,339.42857142857144;I/flutter ( 8341):        │   -2.4492935982947064e-16,1.0,0.0,611.4285714285714;I/flutter ( 8341):        │   0.0,0.0,1.0,0.0; 0.0,0.0,0.0,1.0]I/flutter ( 8341):        │ label: "Increment"I/flutter ( 8341):        │ textDirection: ltrI/flutter ( 8341):        │I/flutter ( 8341):        └─SemanticsNode#12I/flutter ( 8341):            merged up ⬆️I/flutter ( 8341):            Rect.fromLTRB(0.0, 0.0, 56.0, 56.0)I/flutter ( 8341):            actions: tapI/flutter ( 8341):            flags: isButton, hasEnabledState, isEnabledI/flutter ( 8341):            thicknes: 6.0I/flutter ( 8341):

好了,内容就讲到这里。

在实际的开发过程中,App 的界面通常会比上述示例要复杂得多。在运用这些调试工具时,建议大家不要拘泥于其中某一种方式。能够灵活运用工具,才是打造一款优秀的 App 的重要前提。

今天的分享就到这里,希望这些内容能够对你有帮助,我们下次再见。




"开发者说·DTalk" 面向中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。


 点击屏末 |  | 了解更多 "开发者说·DTalk" 活动详情与参与方式


长按右侧二维码

报名参与




登录查看更多
0

相关内容

Google 发布的面向结构化 web 应用的开语言。 dartlang.org
【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
57+阅读 · 2020年6月26日
【实用书】Python爬虫Web抓取数据,第二版,306页pdf
专知会员服务
117+阅读 · 2020年5月10日
TensorFlow Lite指南实战《TensorFlow Lite A primer》,附48页PPT
专知会员服务
69+阅读 · 2020年1月17日
【干货】大数据入门指南:Hadoop、Hive、Spark、 Storm等
专知会员服务
95+阅读 · 2019年12月4日
【电子书】C++ Primer Plus 第6版,附PDF
专知会员服务
87+阅读 · 2019年11月25日
【电子书】Flutter实战305页PDF免费下载
专知会员服务
22+阅读 · 2019年11月7日
【初学者系列】tensorboard学习笔记
专知
7+阅读 · 2019年10月4日
用Now轻松部署无服务器Node应用程序
前端之巅
16+阅读 · 2019年6月19日
美团:基于跨平台框架Flutter的动态化平台建设
前端之巅
14+阅读 · 2019年6月17日
7 款实用到哭的App,只说一遍
高效率工具搜罗
84+阅读 · 2019年4月30日
已删除
创业邦杂志
5+阅读 · 2019年3月27日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
Tensorflow Eager Execution入门指南
专知
6+阅读 · 2018年4月16日
A Survey on Edge Intelligence
Arxiv
51+阅读 · 2020年3月26日
Arxiv
92+阅读 · 2020年2月28日
Arxiv
136+阅读 · 2018年10月8日
Arxiv
6+阅读 · 2018年5月18日
VIP会员
相关VIP内容
相关资讯
【初学者系列】tensorboard学习笔记
专知
7+阅读 · 2019年10月4日
用Now轻松部署无服务器Node应用程序
前端之巅
16+阅读 · 2019年6月19日
美团:基于跨平台框架Flutter的动态化平台建设
前端之巅
14+阅读 · 2019年6月17日
7 款实用到哭的App,只说一遍
高效率工具搜罗
84+阅读 · 2019年4月30日
已删除
创业邦杂志
5+阅读 · 2019年3月27日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
Tensorflow Eager Execution入门指南
专知
6+阅读 · 2018年4月16日
相关论文
A Survey on Edge Intelligence
Arxiv
51+阅读 · 2020年3月26日
Arxiv
92+阅读 · 2020年2月28日
Arxiv
136+阅读 · 2018年10月8日
Arxiv
6+阅读 · 2018年5月18日
Top
微信扫码咨询专知VIP会员