Flutter编译模式
Flutter 支持 3 种运行模式,包括 Debug、Release 和 Profile,在编译时,这三种模式是完全独立的;
- Debug 模式对应 Dart 的 JIT 模式,可以在真机和模拟器上同时运行。该模式会打开所有的断言(assert),以及所有的调试信息、服务扩展和调试辅助(比如 Observatory)。此外,该模式为快速开发和运行做了优化,支持亚秒级有状态的 Hot reload(热重载),但并没有优化代码执行速度、二进制包大小和部署。flutter run --debug 命令,就是以这种模式运行的。
- Release 模式对应 Dart 的 AOT 模式,只能在真机上运行,不能在模拟器上运行,其编译目标为最终的线上发布,给最终的用户使用。该模式会关闭所有的断言,以及尽可能多的调试信息、服务扩展和调试辅助。此外,该模式优化了应用快速启动、代码快速执行,以及二级制包大小,因此编译时间较长。flutter run --release 命令,就是以这种模式运行的。
- Profile 模式,基本与 Release 模式一致,只是多了对 Profile 模式的服务扩展的支持,包括支持跟踪,以及一些为了最低限度支持所需要的依赖(比如,可以连接 Observatory 到进程)。该模式用于分析真实设备实际运行性能。flutter run --profile 命令,就是以这种模式运行的。
Flutter 开发调试工具
输出日志
print() 、debugPrint() 这两个函数打印日志;
断点调试
使用的AndroidStudio,调试跟开发原生一样
Debug 模式断言
我们在断言里传入了一个始终返回 true 的匿名函数执行结果,这个匿名函数的函数体只会在 Debug 模式下生效,Flutter源码中很多地方使用这种方式在Debug模式下校验;
assert(() {
/// todo
return true;
}());
布局调试
- debugPaintSizeEnabled = true; 能够以辅助线的方式,清晰展示每个控件元素的布局边界,可以根据辅助线快速找出布局出问题的地方;
- Open DevTools 获取到 Widget 的可视化信息(比如布局信息、渲染信息)等
Flutter 性能分析
在 Flutter 中,性能问题可以分为 GPU 线程问题和 UI 线程(CPU)问题两类
UI渲染时间都去哪里了
上面两个图上面的是原生Adnroid、ios,下面是Flutter UI渲染过程,可以看到基本上都是差不多的,UI绘制耗时主要发生在测量、布局、绘制阶段;
Flutter build 、 layout 、 paint性能阶段
下面通过一个demo例子分析Flutter build 、 layout 、 paint阶段,界面是默认创建Fulutter项目的例子,改成每过一秒自动刷新count++ 、刷新界面,(去除FloatingActionButton点击水波纹动画的影响);
void main() {
debugProfileBuildsEnabled = true;
//debugPrintLayouts = true;
// debugPaintLayerBordersEnabled = true;
// debugRepaintRainbowEnabled = true;
// debugProfilePaintsEnabled = true;
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
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;
@override
void initState() {
super.initState();
Timer.periodic(Duration(seconds: 1), (timer) {
if(mounted)
setState(() {
_counter++;
});
});
}
@override
void dispose() {
if (_timer != null) {
_timer.cancel();
}
super.dispose();
}
@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.headline4,
)
],
),
),
);
}
}
Build 阶段
通过Flutter提供的可视化工具Observatory timeline Build阶段查看
- 可以看到 build阶段相比于layout、paint 阶段耗时要长的;
- 每一帧刷新会把MyHomePage所有的widget重新构建了一遍,因为Flutter把widget设计成不可变的; 分析:我们只是改变了红色圈里的Text widget 的文本,但是build创建了MyHomePage的所有widget;这是最简单的界面,如果界面稍微复杂耗时更多;这也是我们平时写代码容易犯错的,因为我们是在_MyHomePageState调用的setState(),Flutter就会把MyHomePage对应的StatefulElement标脏,所以刷新整个MyHomePage;
仔细分析下界面刷新基本上就有三种情况:
- setState() 刷新;
- InheritedWidget 依赖改变刷新
- Hot Reload 热重载(只用到开发者开发阶段); 解决问题就是 局部刷新,也就是控制 Build 的粒度,只构建刷新的部分;实现局部刷新可以使用 以通过 provider 、GetX、flutter_bloc等状态管理库实现
provider 封装 InheritedWidget
GetX(最近很火,简单,功能更多)
上面是在pub 上相对活跃的状态管理的库;但是如果一个界面只是更改一下文本引入使用库,相对较重;
- 我们也可以通过提取widget组件抽离,例如上面的例子,可以将发生改变的Text 单独提取到StatefulWidget中;
- Flutter 框架内部也提供专门用于局部组件的刷新,ValueListenableBuilder,其实ValueListenableBuilder的本质也是组件抽离; 对上面的例子使用ValueListenableBuilder优化
void main() {
debugProfileBuildsEnabled = true;
//debugPrintLayouts = true;
// debugPaintLayerBordersEnabled = true;
// debugRepaintRainbowEnabled = true;
// debugProfilePaintsEnabled = true;
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
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> {
final ValueNotifier<int> _counter = ValueNotifier<int>(0);
Timer _timer;
@override
void initState() {
super.initState();
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
if (mounted) _counter.value++;
});
}
@override
void dispose() {
if (_timer != null) {
_timer.cancel();
}
super.dispose();
}
@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:',
),
ValueListenableBuilder(
valueListenable: _counter,
builder: (ctx, value, child) {
return Text(
'$value',
style: Theme.of(context).textTheme.headline4,
);
},
),
],
),
),
);
}
}
再看下Observatory timeline Build阶段
对比前后可以看到build,使用ValueListenableBuilder更改后,build只是从ValueListenableBuilder开始刷新创建的
Flutter 也提供ValueListenableBuilder类似局部刷新的Widget
- FutureBuilder
- StreamBuilder
- AnimatedBuilder(里面有一个child属性,我们设置可以跟动画无关的widget,复用不用每一帧都创建,要知道动画每秒60hz的话,每秒要创建60个没有必要的widget的对象)
layout阶段
在Observatory timeline 我没有找到layout阶段打印分析??? debugPrintLayouts = true;可以在控制台输出需要重新布局的RenderObject;
layout阶段主要是当有一个节点被标脏,有多少其他节点跟着一起重新布局;会涉及到布局边界的优化;
布局边界这块开发者基本上不会犯错误,因为这块Flutter 框架帮我们做了优化;自动在合适的地方设置布局边界,如果在自定义widget的时候,自定义RenderObject,可以考虑widget是否可以触发布局边界,提高性能的优化;
触发布局边界的条件
void layout(Constraints constraints, { bool parentUsesSize = false }) {
RenderObject relayoutBoundary;
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
relayoutBoundary = this;
} else {
relayoutBoundary = (parent as RenderObject)._relayoutBoundary;
}
}
paint阶段
Paint阶段是当有一个节点被标脏,有多少其他节点跟着一起重新绘制;就会涉及到图层概念,绘制边界;
Flutter 会让应用的分界线形成自己图层就比如 SingleChildScrollView 屏幕外 跟屏幕内在不同的图层里;
上面的例子我们打开debugProfilePaintsEnabled = true;查看timeline
可以看到Text 文本更新涉及到很多节点跟着一起被标脏了,因为他们在一个图层里面;虽然Flutter 也做了diff优化,因为RenderObject是一个长的生命周期对象,在updateRenderObject()setter中会做优化,就比如下面是RenderDecoratedBox的setter decoration方法;如果前后没有变化就直接返回了,没有去重绘;
set decoration(Decoration value) {
assert(value != null);
if (value == _decoration)
return;
_painter?.dispose();
_painter = null;
_decoration = value;
markNeedsPaint();
}
如果一个界面很复杂,有一块区域一直在刷新,比如一块区域一直在做动画,而且不影响其他的部分的话,就算Flutter 会对Paint阶段做diff优化,会涉及到大量的遍历;也是会增加paint的时间;flutter 提供了RepaintBoundary自己会c闯将一个单独的图层;隔离起来;
使用RepaintBoundary将更改widget的包裹起来
可以看到更新只在这个图层的更新
GPU 渲染性能分析
可以看到Flutter 性能为什么能媲美原生或者超越原生的原因;
原生app绘制的流程,Android框架java代码--->> skia(c/c++)绘图引擎--->>cpu/gpu指令--->>设备显示绘图; Flutter框架dart代码完全取代了原生java代码,而且skia作为Flutter sdk的一部分;Flutter sdk升级很快也很方便;skia性能的优化很快体现到Flutter性能中;
分析性能要使用profile模式去验证,用真机不要用模拟机;
flutter run --profile
Flutter 应用skia api的调用
flutter run --profile --trace-skia
可以看到每个帧的消耗时间,每个函数消耗时间及调用次数;
如果遇到性能问题,UI渲染优化后,还是会卡顿,我们可以针对分析调用skia api函数消耗的时间及次数做优化;
捕捉每一条SKPicture绘制指令
flutter screenshot --type=skia --observatory-uri="生成的本地uri";
生成一个skp格式的文件在项目根目录;然后上传文件到 debugger.skia.org/ (需翻墙)进行分析。可以播放暂停逐条的绘图指令逐一分析;
耗时的skia 函数
saveLayer、ClipPath、Opacity、ShaderMask、ColorFilter、PhysicalModel、BackdropFilter等使用,项目中最好不要用很耗性能的widget或者函数;
比如如果使用ClipPath;Flutter文档也提示了是一个昂贵耗时的操作,可以选择ClipRRect、ClipRRect代替等
/// A widget that clips its child using a path.
///
/// Calls a callback on a delegate whenever the widget is to be
/// painted. The callback returns a path and the widget prevents the
/// child from painting outside the path.
///
/// Clipping to a path is expensive. Certain shapes have more
/// optimized widgets:
///
/// * To clip to a rectangle, consider [ClipRRect].
/// * To clip to an oval or circle, consider [ClipOval].
/// * To clip to a rounded rectangle, consider [ClipRRect].
///
/// To clip to a particular [ShapeBorder], consider using either the
/// [ClipPath.shape] static method or the [ShapeBorderClipper] custom clipper
/// class.