在我们讨论如何对 Flutter 进行性能优化之前,首先得掌握 Flutter 的渲染原理,这样才能更好的对症下药。本文将主要讲讨论 UI 线程中的性能优化,由于 GPU 线程涉及底层 Skia 图形引擎的调用,相较于 UI 线程而言更加繁琐,对其感兴趣的同学可以观看 Google 官方的《深入了解 Flutter 的高性能图形渲染》。
渲染流程图.png
根据上图,我们可知 Flutter 的主要渲染流程:在初次渲染时,我们会根据我们自己的业务代码,分别构建 Widget、 Element 以及 RenderObject 三棵树,其次对 RenderObjective Tree 的每个节点进行遍历,再对发生改变的节点处进行标脏处理,执行 paint 操作,形成一个 Layer Tree,最后把形成好的 Layer Tree 发送给 GPU 线程,GPU 线程在接收到 Layer Tree 之后,将 Layer Tree 转成为 GPU 的可执行指令。
我们在命令行中输入flutter run --profile
的指令,即可在 profile 模式下对我们的应用进行调试,在执行该命令后会产生一个链接,打开该链接后如下图所示。我们在 UI 线程和 GPU 线程调试都会用到该页面
UI 线程和 GPU 线程观测所进行的操作是不同的,具体不同如下:
flutter run --profile --trace-skia
运行我们的应用,然后继续通过 timeline 去进行分析,不过 Record Streams Profile 的值换成了 All,这里可以查看到许多的 Skia 函数的调用,我们可以分析每一个 Skia 函数的调用次数。观测台
timeline
架构对比
上面这张图我们可以很清楚看到,Flutter 框架可以直接调用 Skia 图形引擎,这也是 Flutter 性能能够媲美原生的重要原因;而不是像 react-native 那样首先得先通过 JSBridge 调用 Java 代码,然后再通过 Java 代码去调用 Skia 图形引擎,相较于 Flutter 多一层调用,所以性能也会存在丢失。
至此,我们可知 Flutter 在 UI 线程中渲染主要涉及到 build、 layout 以及 paint 阶段,我们下面将会根据这三个阶段来介绍的具体过程以及性能优化方式。
Element Tree 中的 Element 主要涉及到两种类型,分别是:
其中,ComponentElement 主要做组合,不会直接参与布局。而 RendObjectElement 则用来对 RenderObject 树上的 RenderObject 节点做连接。当我们对 Widget 树里面的某一个节点进行更新时,因为 Widget 是不可改变的,所以我们在改变的时候,只能扔掉旧的树,然后重新去创建一个新的 Widget 树;在创建完新的 Widget 树之后,再对上一帧的 Element 树做遍历,在 Element 类上有一个 updateChild 的方法,它可以对子节点进行比较并操作,通过查看当前的子节点类型和上一帧的子节点类型是否一致,如果不一致,直接扔掉创建一个新的 Element 节点,反之则对自己的子节点进行更新。
即:
我们提高 build 效率的核心本质是:
在具体的实际业务开发中,我们可以在代码的任意处加上debugProfileBuildsEnabled = true
,这可以帮助我们通过 timeline 发现 build 过程中的具体性能瓶颈。如下图所示,timeline 中可以清晰的看到 build 更新时哪些节点发生了遍历,再根据图中找到我们应用的性能瓶颈。
build 阶段 timeline
在我们业务开发中,我们遵循以下方法,可以有效的控制 build 的耗时:
实例代码
class BeginWidget extends StatefulWidget{
BeginWidget({Key key}):super(key: key);
@override
_beginWidgetState createState() => _beginWidgetState();
}
class _beginWidgetState extends State<BeginWidget>{
int time = 60;
@override
void initState(){
super.initState();
if(mounted){
setState((){
time = time - 1;
});
}
}
Widget build(BuildContext context) {
return Column(
children: [
Expanded(child: Container(child: Text('123'))),
Expanded(
child: Container(
width: 100,
height: 100,
child:Row(
children: [
Text('倒计时'),
Text('$time')
],
),
decoration: BoxDecoration(color: Colors.blue),
)
)
],
);
}
}
优化后代码:
class AfterWidget extends StatelessWidget{
const AfterWidget({Key key}) : super(key: key);
Widget build(BuildContext context){
return Column(
children: [
Expanded(child: Container(child: Text('123'))),
Expanded(
child: Container(
width: 100,
height: 100,
child:Row(
children: [
Text('倒计时'),
TimeWidget(),
],
),
decoration: BoxDecoration(color: Colors.blue),
)
)
],
);
}
}
class TimeWidget extends StatefulWidget{
TimeWidget({Key key}):super(key: key);
@override
_timeWidgetState createState() => _timeWidgetState();
}
class _timeWidgetState extends State<TimeWidget>{
int time = 60;
@override
void initState(){
super.initState();
if(mounted){
setState((){
time = time - 1;
});
}
}
Widget build(BuildContext context) {
return Text('$time');
}
}
上述代码通过将 Widget 的粒度细化,能够有效地降低遍历的起点位置。当 Widget 数过于复杂时,我们应该尽量将 Widget 抽离出来,单个 Widget 树最好不要太多,这样既有利于提高代码的可读性,也有利于 Widget 树更新时,避免不必要的 Widget 节点重新构建。
Selector(
selector: (context, DataModel dataModel) {
return dataModel.xxx;
},
builder: (context, xxx, child) {
return Container(
child: Row(
Text(xxx),
child,
),
);
},
child: Container(child: Text('123')),
)
在使用 Provider 的 Selector 类时,其 build 的 child 参数就是通过提前结束子树的遍历来进行性能优化的,当数据更新时,Widget 树将重新进行构建,遇到 child 的地方直接将之前写好的 child 树连接上,当然这不会对 child 里面的节点进行重新遍历。Provider 通过 Selector 代替 Consumer 本身也是一种提高性能的方式,它是通过上面所说的降低遍历的起始点,使得在数据更新后,对极小需要更新数据的地方重新进行遍历。除 Selector 之外,还有许多地方都有提供 child 的属性,他们大多数的目的都是为了能够让 Widget 复用,提前结束子树的遍历。
layout 的过程主要是为了计算出节点真正所占的大小。在建立 layout tree 的过程中,首先父节点会给出一个宽高大小的限制,然后子节点再来决定自己的大小。在 Layout 中存在一个 Relayout boundary 的概念,它可以产生一个边界,确保在边界内的布局发生改变时,不会让边界外的部分也重新计算,这样也可以在某些特定情况下提高我们应用的性能。除此之外,在我们书写 Widget 的时候,如果能够给出 Widget 宽高的话,尽量给出来,因为在布局中,宽度的计算也会占用一定的时间。比如在使用 ListView 这样的滑动组件时,我们应该给出滑块的高度,即 itemExtend 的值,这样在滑动的时候,UI 线程不会花费大量的时间在计算高度上。
class ListViewShow extends StatelessWidget {
const ListViewShow({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView(
itemExtent: 30, // 指定 children 每一个 child 的高度
children: <Widget>[
Container(
child: Text('ListView 1'),
),
Container(
child: Text('ListView 2'),
),
Container(
child: Text('ListView 3'),
),
],
);
}
}
在 RenderObject 标脏后,paint 会对已经标脏的 RenderObject 图层重新进行绘制。这里和 Layout 相似,存在一个 Repaint boundary 的概念,它的原理和 layout 里面的 Relayout boundary 的基本相似,区别是它在 paint 的时候产生一个边界,防止页面大范围重新绘制。如果页面是频繁更新的页面,例如包含定时器的页面,在使用倒计时这样的控件时,我们可以在最小控件范围外包一层 RepaintBoundary 来与周围图层进行隔离。同 build 阶段一样,我们可以在代码里面加入debugProfilePaintsEnabled = true
来在 timeline 里面观看 paint 阶段有哪些不必要的图层发生了更新。
paint 阶段 timeline
import 'package:flutter/material.dart';
import 'dart:async';
class HoursTitle extends StatefulWidget {
@override
HoursTitleState createState() => HoursTitleState();
}
class HoursTitleState extends State<HoursTitle> {
static Timer timer;
int minutes = DateTime.now().minute;
int seconds = DateTime.now().second;
Duration duration = Duration(seconds: 1);
void _startTimer() {
timer?.cancel();
timer = Timer.periodic(duration, (timer) {
if (seconds == 0) {
if (minutes == 0) {
minutes = 59;
seconds = 59;
} else {
minutes--;
seconds = 59;
}
} else {
seconds--;
}
setState(() {
minutes = minutes;
seconds = seconds;
});
});
}
@override
void initState() {
super.initState();
if (!mounted) {
return;
}
_startTimer();
}
@override
void dispose() {
super.dispose();
timer?.cancel();
}
Widget build(BuildContext context) {
// 通过RepaintBoundary增加一个绘制边界
return Container(
child: Row(
children: [
RepaintBoundary(
child: Container(
width: 200,
height: 30,
child: Text.rich(
TextSpan(
text: '距本小时结束',
children: [
TextSpan(
text: '$minutes : $seconds',
),
],
),
),
),
),
Text('123'),
Text('465'),
],
),
);
}
}
这里的定时器只会导致 RepaintBoundary 包裹的 Widget 重新绘制,不会导致到周围其他的 Widget 的重新绘制,这在图层很大的时候,会非常有用,当然 Flutter 的一些组件页支持了图层划分,比如 ListView 里面的 isRepaintBoundary 属性,可以直接帮我们合成视图,避免了不必要的重新 paint。
Flutter 性能优化涉及到方方面面,本文从渲染原理的角度进行切入讲解其优化手段。还有 Flutter 组件选择等其他方面也是有所讲究的,例如 ListView 和 ListView.builder 之间的选择;还有在实际的业务开发中,对于 Opacity 这样大量消耗性能的 Widget 最好尽量少用,因为它会调用saveLayer()
方法,这个方法它会很大程度上影响 GPU 线程的效率。至于其后章节,笔者未来会出文进行全面讲解,请期待该系列的下一篇文章。