在使用flutter run
命令来运行应用程序以前,请运行flutter analyze
命令来检测你的代码,这个命令是Dart Analyzer(分析器)的一个包装,它将分析你的代码并帮助你发现可能出现的错误。由于Dart Analyzer分析器大量使用了代码中的类型注释来帮助追踪问题,因此笔者鼓励你们在任何地方任什么时候候都来使用它检测你的代码,从而避免var、无类型的参数和无类型的列表文字等问题,能够说它是追踪问题的最快的方式。html
使用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);
// ...
}
复制代码
print
、debugPrint
、flutter 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
在开发过程当中,强烈建议你使用Flutter的“调试(debug)”模式,有时也称为“检查(checked)”模式(注意:Dart2.0后“checked”模式被废除,可使用“strong”模式)。若是你使用flutter run
运行程序,“调试”模式是默认的,在这种模式下,Dart assert语句被启用,Flutter框架使用它来执行许多运行时检查、验证赋值是否合法。当一个赋值不合法时,它会向控制台报告,并提供一些上下文信息来帮助追踪问题的根源。app
要关闭“调试(debug)”模式并使用“发布(release)”模式,请使用flutter run --release
运行你的应用程序,不过这样也关闭了Observatory调式器,一个中间模式能够关闭除Observatory调试器以外的全部调试辅助工具,称为“profile”模式,用--profile
替代--release
便可。
Flutter框架的每一层都提供了将其当前状态或事件,转储(dump)到控制台(使用debugPrint
)的功能。
要转储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”是TapGestureDetector
的toStringShort()
方法的输出)。
若是编写本身的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")
复制代码
要找出相对于帧的开始/结束事件发生的位置,能够切换debugPrintBeginFrameBanner
和debugPrintEndFrameBanner
的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
还能够用来打印致使当前帧被调度的调用堆栈。
你也能够经过将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...”开头的变量或方法,都只能在调试模式下有效。
调试动画最简单的方法是放慢它们的速度。为此,请将timeDilation
变量(在scheduler库中)设置为大于1.0的数字,例如50.0。最好在应用程序启动时只设置一次,若是在运行中更改它,尤为是在动画运行时将其值变小,则框架可能会观察到时间倒退,这可能会致使断言而且一般会干扰你的工做。
要了解致使你的应用程序从新布局或从新绘制的缘由,能够分别设置debugPrintMarkNeedsLayoutStacks
和debugPrintMarkNeedsPaintStacks
标志。每当渲染框被要求从新布局或从新绘制时,这些都会随时将堆栈跟踪日志打印到控制台上。若是这种方法对你有用,你可使用services
库中的debugPrintStack()
方法按需打印堆栈痕迹。
要收集有关Flutter应用程序启动所需时间的详细信息,能够在运行flutter run
命令时使用trace-startup
和profile
选项。
$ flutter run --trace-startup --profile
复制代码
跟踪日志被保存在你的Flutter工程目录下的build目录下的start_up_info.json
文件中。日志输出列出了从应用程序启动到这些跟踪事件(以微秒捕获)所用的时间:
例如:
{
"engineEnterTimestampMicros": 96025565262,
"timeToFirstFrameMicros": 2171978,
"timeToFrameworkInitMicros": 514585,
"timeAfterFrameworkInitMicros": 1657393
}
复制代码
要执行自定义性能跟踪并测量Dart任意代码块的wall/CPU时间(相似于在Android上使用systrace)。可使用dart:developer
的Timeline工具来包含你想测试的代码块,例如:
Timeline.startSync('interesting function');
// iWonderHowLongThisTakes();
Timeline.finishSync();
复制代码
而后打开你的应用程序的Observatory timeline页面,在“Recorded Streams”中选择‘Dart’复选框,并执行你想测试的功能。刷新该页面,将会在Chrome浏览器的跟踪工具中按时间顺序显示应用程序的时间轴记录。
务必使用flutter run --profile
命令运行应用程序,以确保运行时性能特征与你的最终产品的性能差别最小。
要得到应用程序性能图形视图,请将MaterialApp
构造函数的showPerformanceOverlay
参数设置为true
。WidgetsApp
构造函数也有相似的参数(若是你没有使用MaterialApp
或者WidgetsApp
,你能够经过将你的应用程序封装在一个堆栈中,调用new PerformanceOverlay.allEnabled()
方法建立一个控件放在堆栈上来得到相同的效果)。
这将显示两个图表,第一个是GPU线程花费的时间,第二个是CPU线程花费的时间。图中的白线以16ms增量沿纵轴显示,若是图表通过其中的一条白线,那么你的运行频率(速度)低于60Hz,横轴表明帧。该图表只会在应用程序绘制时更新,因此若是它处于空闲状态,该图表将中止移动。
这个操做必定是在发布模式下完成的,由于在调试模式下,会故意牺牲性能来换取有助于开发调试的功能,如assert声明,这些都是很是耗时的,所以结果将会产生误导。
当咱们开发实现Material Design
的应用程序时,应用程序上会覆盖一个帮助验证对齐的Material Design
基线网格。为此,在调试模式下,将MaterialApp
构造函数的debugShowMaterialGrid
参数设为true
,将会覆盖这样一个网格。也能够直接使用GridPaper
控件在非Material
应用程序上覆盖这样的网格。