Flutter渲染原理简介
优化之前我们先来介绍下Flutter的渲染原理,通过这部分基础了解渲染流程以及主要耗时花费
flutter视图树包含了三颗树:Widget、Element、RenderObject
Widget
:存放渲染内容
,它只是一个配置数据结构,创建是非常轻量的,在页面刷新的过程中随时会重建Element
: 同时持有Widget
和RenderObject
,存放上下文信息
,通过它来遍历视图树,支撑UI结构RenderObject
: 根据Widget的布局属性进行layout
,paint
,负责真正的渲染
从创建到渲染的大体流程是:根据Widget生成Element
,然后创建
相应的RenderObject
并关联
到Element.renderObject
属性上,最后再通过RenderObject
来完成布局排列和绘制
。
例如下面这段布局代码
Container(
color: Colors.blue,
child: Row(
children: [
Image.asset('image'),
Text('text'),
],
),
);
对应三棵树的结构如下图
了解了这三棵树,我们再来看下页面刷新的时候具体做了哪些操作
当需要更新UI
的时候,Framework通知Engine
,Engine会等到下个Vsync信号
到达的时候,会通知Framework进行animate、build、layout、paint
,最后生成layer
提交给Engine
。Engine会把layer进行组合,生成纹理
,最后通过Open GL
接口提交数据给GPU
, GPU经过处理后在显示器上面显示,如下图所示:
结合前面的例子,如果text文本或者image内容发生变化会触发哪些操作呢?
Widget
是不可改变
,需要重新创建一颗新树
,build开始,然后对上一帧的Element树
做遍历,调用他的updateChild
,看子节点类型跟之前是不是一样,不一样
的话就把子节点扔掉
,创造
一个新的
,一样
的话就做内容更新
。对renderObject
做updateRenderObject
操作,updateRenderObject
内部实现会判断现在的节点跟上一帧是不是有改动
,有改动才会标记dirty
,重新layout、paint
,再生成新的layer
交给GPU
,流程如下图:
性能分析工具及方法
下面来看下性能分析工具,注意,统计性能数据一定要在真机+profile模式
下运行,拿到最接近真实的体验数据。
performance overlay
平时常用的性能分析工具有performance overlay
,通过它可以直观看到当前帧的耗时,但是它是UI线程
和GPU线程``分开展示
的,UI Task Runner
是Flutter Engine
用于执行Dart root isolate
代码,GPU Task Runner
被用于执行设备GPU
的相关调用。绿色
的线表示当前帧
,出现红色
则表示耗时超过16.6ms
,也就是发生丢帧
现象
Dart DevTool
另一个工具是Dart DevTool ,就是早期的Observatory,官方提供的性能检测工具。它的 timeline 界面可以让逐帧分析应用的 UI 性能。但是目前还是预览版,存在一些问题。
profile模式下运行起来,点击android studio底部的菜单按钮,会弹出一个网页
点击顶部的Timeline菜单
这个时候滑动页面,每一帧的耗时会以柱形bar
的形式显示在页面上,每条bar
代表一个frame
,同时用不同颜色区分UI/GPU
线程耗时,这个时候我们要分析卡顿的场景就需要选中一条红色的bar(总耗时超过16.6ms)
,中间区域的Frame events chart
显示了当前选中的frame的事件跟踪
,UI
和GPU
事件是独立
的事件流,但它们共享
一个公共的时间轴
。
选中Frame events chart
中的某个事件,以上图为例Layout耗时最长,我们选中它,会在底部Flame chart
区域显示一个自顶向下
的堆栈跟踪
,每个堆栈帧的宽度
表示它消耗CPU的时长
,消耗大量CPU时长的堆栈是我们首要分析的重点,后面就是具体分析堆栈,定位卡顿问题。
debug调试工具
另外还有一些debug调试工具
可以辅助查看更多信息,注意,只能在debug模式
下使用分析,拿到的数据不能作为性能标准
debugProfileBuildsEnabled
:向 Timeline 事件中添加
每个widget
的build 信息
debugProfilePaintsEnabled
: 向 timeline 事件中添加
每个renderObject
的paint 信息
debugPaintLayerBordersEnabled
:每个layer
会出现一个边框
,帮助区分layer层级
debugPrintRebuildDirtyWidgets
:打印标记
为dirty
的widgets
debugPrintLayouts
:打印标记
为dirty
的renderObjects
debugPrintBeginFrameBanner/debugPrintEndFrameBanne
r:打印每帧开始
和结束
实例分析
了解这些工具下面我们来看个简单的demo具体分析下,一个由Column、Container、ListView嵌套的布局,其中有个定时器控制Text中显示的文本实时更新
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class TestDemo extends StatefulWidget {
@override
State createState() {
return _TestDemoState();
}
}
class _TestDemoState extends State {
int _count = 0;
Timer _timer;
@override
void initState() {
super.initState();
_timer = Timer.periodic(Duration(milliseconds: 1000), (t) {
setState(() {
_count++;
});
});
}
@override
void dispose() {
if (_timer != null) {
if (_timer.isActive) {
_timer.cancel();
}
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Test Demo"),
),
body: content()
);
}
Widget content(){
Widget result = Column(
children: [
Container(
margin: EdgeInsets.fromLTRB(10,10,10,5),
height: 100,
color: Color(0xff1fbfbf),
),
Container(
margin: EdgeInsets.fromLTRB(10,5,10,10),
height: 100,
color: Color(0xff1b8bdf),
),
Container(
height: 100,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 5,
itemBuilder: (context, index) {
return Container(
width: 70,
height: 70,
child: Image.asset(
'common.png',
width: 50,
height: 50,
),
);
}),
),
Container(
margin: EdgeInsets.fromLTRB(10,20,10,10),
height: 100,
width: 350,
color: Colors.yellow,
child: Center(
child:
Text(
_count.toString(),
style: TextStyle(fontSize: 18, fontWeight:FontWeight.bold),
),
)
),
],
);
return result;
}
}
大部分widget都是静态的,只有黄色Container中包含一个内容一直刷新的Text,这个时候我们打开debugProfileBuildsEnabled,用Timeline分析下它的渲染耗时,可以通过Frame events chart看到显示的build层级非常深
结合第一部分渲染原理我们了解到,每次定时器刷新text数字
的时候,整个页面widget树
都会重新build
,但其实只有最底层Container中的Text内容在改变,没有必要刷新整颗树,所以这里我们的优化方案是提高build效率
,降低Widget tree遍历的出发点
,将setState
刷新数据尽量下发到底层节点
,所以将Text单独
抽取成独立的Widget
,setState下发到抽取出的Widget内部
class _TestDemoState extends State {
...
Widget content(){
Widget result = Column(
children: [
...
Container(
margin: EdgeInsets.fromLTRB(10,20,10,10),
height: 100,
width: 350,
color: Colors.yellow,
child: Center(
child:
CountText()
)
),
],
);
return result;
}
}
class CountText extends StatefulWidget {
@override
State createState() {
return _CountTextState();
}
}
class _CountTextState extends State {
int _count = 0;
Timer _timer;
@override
void initState() {
super.initState();
_timer = Timer.periodic(Duration(milliseconds: 1000), (t) {
setState(() {
_count++;
});
});
}
@override
void dispose() {
if (_timer != null) {
if (_timer.isActive) {
_timer.cancel();
}
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Text(
_count.toString(),
style: TextStyle(fontSize: 18, fontWeight:FontWeight.bold),
);
}
}
修改后的Timeline显示如下图:
可以看到build层级
明显减少
,总耗时
也明显降低
接下来分析下Paint过程
有没有可以优化的部分,我们打开debugProfilePaintsEnabled
变量分析可以看到Timeline显示的paint层级
通过debugPaintLayerBordersEnabled = true;
显示layer边框可以看到不断变化的Text和其他Widget都是在同一个layer中的,这里我们想到的优化点是利用RepaintBoundary提高paint效率,它为经常发生显示变化的内容提供一个新的隔离layer,新的layer paint不会影响到其他layer
RepaintBoundary(
child: Container(
margin: EdgeInsets.fromLTRB(10,20,10,10),
height: 100,
width: 350,
color: Colors.yellow,
child: Center(
child: CountText()
)
),
)
优化后的效果如下:
可以看到我们为黄色的Container建立了单独的layer,并且paint的层级减少很多。
总结常见问题
提高build效率
,setState刷新数据尽量下发到底层节点提高paint效率
,RepaintBoundry创建单独layer,减少重绘区域减少build中逻辑处理
,因为widget在页面刷新的过程中随时会通过build重建,build调用频繁,我们应该只处理跟UI相关的逻辑减少saveLayer(ShaderMask、ColorFilter、Text Overflow)、clipPath的使用
,saveLayer会在GPU中分配一块新的绘图缓冲区,切换绘图目标,这个操作是在GPU中非常耗时的,clipPath会影响每个绘图指令,将相交操作之外的部分剔除掉,所以这也是个耗时操作减少Opacity Widget 使用
,尤其是在动画中,因为他会导致widget每一帧都会被重建,可以用AnimatedOpacity
或FadeInImage
进行代替