目录
0 引言
本文是对第二版序 | 《Flutter实战·第二版》 (flutterchina.club)的学习和总结。
1 调试Flutter应用
1.1 日志与断点
1.1.1 debugger()
声明
当使用Dart Observatory(调试模式时自动启用),可以使用debugger()
语句插入编程式断点。
import 'dart:developer';
/*
debugger()语句采用一个可选的when参数,可以指定该参数仅在特定条件为真时中断
*/
void someFunction(double offset) {
debugger(when: offset > 30.0);
// ...
}
1.1.2 print和
debugPrint
- Dart
print()
功能将输出到系统控制台,可以使用flutter logs
来查看它。如果一次输出太多,Android有时会丢弃一些日志行。- 推荐使用Flutter的
foundation
库中的debugPrint(),它封装了 print,将一次输出的内容长度限制在一个级别(内容过多时会分批输出),避免被Android内核丢弃。
1.1.3 调试模式、中间模式、发布模式
- 在Flutter应用调试过程中,Dart
assert
语句被启用,当某个规则被违反时,就会在控制台打印错误日志,并带上一些上下文信息来帮助追踪问题的根源。- 发布模式会关闭Observatory调试器,使用
flutter run --release
运行应用程序即可打开发布模式。- 中间模式“profile mode”可以关闭除Observatory之外所有的调试辅助工具,使用
flutter run --profile
运行应用程序即可打开中间模式。
1.1.4 断点
开发过程中,断点是最实用的调试工具之一,以 Android Studio 为例:比如在 93 行打了一个断点,一旦代码执行到这一行就会暂停,这时我们可以看到当前上下文所有变量的值,然后可以选择一步一步的执行代码。
1.2 调试应用程序层
Flutter框架的每一层都提供了将其当前状态或事件转储(dump)到控制台(使用debugPrint
)的功能。
1.2.1 转储Widgets树
- 要转储Widgets树的状态,请调用debugDumpApp()
- 需要在调用
runApp()
之后调用- 不能在
build()
方法内调用- 不要在布局或绘制阶段调用
- 从frame 回调或事件处理器中调用是最佳方案
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: AppHome(),
),
);
}
class AppHome extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Material(
child: Center(
child: TextButton(
onPressed: () {
// 当按钮从被按下变为被释放时debugDumpApp()被调用
debugDumpApp();
},
child: 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))
... #省略剩余内容
1.2.2 转储渲染树
- 调试布局问题时,转储Widget树可能不够详细,需要转储渲染树
- 要转储渲染树,请调用
debugDumpRenderTree()
- 要调用
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)
... # 省略
1.2.3 转储Layer树
- 调试合成问题,调用debugDumpLayerTree()
- 可以理解为渲染树是可以分层的,而最终绘制需要将不同的层合成起来,而Layer则是绘制时需要合成的层
对于上面的例子,它会输出下图内容。这是根Layer
的toStringDeep
输出的。根部的变换是应用设备像素比的变换; 在这种情况下,每个逻辑像素代表3.5个设备像素。
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
1.2.4 转储语义树
- 语义树:呈现给系统可访问性API的树
- 转储语义树,要调用debugDumpSemanticsTree()
- 要调用debugDumpSemanticsTree() ,必须首先启用辅助功能,例如启用系统辅助工具或
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")
1.2.5 调度(打印帧的开始和结束)
- 切换debugPrintBeginFrameBanner和debugPrintEndFrameBanner布尔值可以将帧的开始和结束打印到控制台,找出相对于帧的开始/结束事件发生的位置。
- debugPrintScheduleFrameStacks可以用来打印导致当前帧被调度的调用堆栈
例如:
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 : ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
1.2.6 可视化调试
所有这些标志只能在调试模式下工作。
通常,Flutter框架中以“debug...
” 开头的任何内容都只能在调试模式下工作。
设置
debugPaintSizeEnabled
为true
以可视方式调试布局问题:
- 这是来自
rendering
库的布尔值。- 它可以在任何时候启用,并在为true时影响绘制。
- 设置它的最简单方法是在
void main()
的顶部设置。当它被启用时,所有的盒子都会得到一个明亮的深青色边框:
- padding(来自widget如Padding)显示为浅蓝色,
- 子widget周围有一个深蓝色框,
- 对齐方式(来自widget如Center和Align)显示为黄色箭头,
- 空白(如没有任何子节点的Container)以灰色显示。
debugPaintBaselinesEnabled做了类似的事情,但对于具有基线的对象,文字基线以绿色显示,表意(ideographic)基线以橙色显示。
debugPaintPointersEnabled标志打开一个特殊模式,任何正在点击的对象都会以深青色突出显示。 这可以帮助我们确定某个对象是否以某种不正确的方式进行hit测试(Flutter检测点击的位置是否有能响应用户操作的widget),例如,如果它实际上超出了其父项的范围,首先不会考虑通过hit测试。
debugPaintLayerBordersEnabled标志调试合成图层,例如以确定是否以及在何处添加
RepaintBoundary
widget。该标志用橙色或轮廓线标出每个层的边界,或者使用debugRepaintRainbowEnabled标志重绘时,该层会被一组旋转色所覆盖。
1.2.7 调试动画
调试动画最简单的方法是减慢它们的速度:
- 将timeDilation (opens new window)变量(在scheduler库中)设置为大于1.0的数字,例如50.0。
- 最好在应用程序启动时只设置一次。
- 如果在运行中更改它,尤其是在动画运行时将其值改小,则在观察时可能会出现倒退。
1.2.8 调试性能问题
- 要了解应用程序导致重新布局或重新绘制的原因,分别设置debugPrintMarkNeedsLayoutStacks和 debugPrintMarkNeedsPaintStacks标志。
- 每当渲染盒被要求重新布局和重新绘制时,这些都会将堆栈跟踪记录到控制台。
- 可以使用
services
库中的debugPrintStack()
方法按需打印堆栈痕迹。
1.2.9 统计应用启动时间
- 要收集有关Flutter应用程序启动所需时间的详细信息,
- 可以在运行
flutter run
时使用trace-startup
和profile
选项。
$ flutter run --trace-startup --profile
跟踪输出保存为
start_up_info.json
,在Flutter工程目录在build目录下。输出列出了从应用程序启动到这些跟踪事件(以微秒捕获)所用的时间:
- 进入Flutter引擎时.
- 展示应用第一帧时.
- 初始化Flutter框架时.
- 完成Flutter框架初始化时.
例如 :
{
"engineEnterTimestampMicros": 96025565262,
"timeToFirstFrameMicros": 2171978,
"timeToFrameworkInitMicros": 514585,
"timeAfterFrameworkInitMicros": 1657393
}
1.2.10 跟踪Dart代码性能
跟踪和测量Dart任意代码段的wall/CPU时间(类似Android上使用systrace):
Timeline.startSync('interesting function');
// iWonderHowLongThisTakes();
Timeline.finishSync();
1.3 官方可视化调试工具DevTools
Flutter DevTools 是一套 Dart 和 Flutter 的性能调试工具。
它将各种调试工具和能力集成在一起,并提供可视化调试界面。
可以用 Flutter DevTools 开发工具来实现的操作:
检查 Flutter 应用程序的 UI 组件布局和状态;
在 Flutter 应用程序中诊断 UI 性能过低的问题;
Flutter 和 Dart 应用的 CPU 性能检测;
为 Flutter 应用进行网络性能检测;
为 Flutter 或 Dart 应用进行源码级的调试;
在 Flutter 或 Dart 命令行应用中测试内存问题;
- 查看有关正在运行的Flutter或Dart命令行应用程序的常规日志和诊断信息。
查看正在运行的 Flutter 或 Dart 的命令行应用程序相关的常规日志和诊断信息。