一、前言
淘特在很多业务场景都使用了 Flutter,加上业务场景本身具有一定的复杂性,使得 Flutter 在低端机流式场景的滑动浏览过程中卡顿、跳帧对比使用原生(Android/iOS)开发明显。通过分析业务层在 Flutter 渲染流程中的每个阶段存在的性能问题进行了一系列的深度优化后,平均帧率已经达到50帧之上超越了原生的表现, 但卡顿率依然达不到最佳的体验效果,遇到了难以突破的瓶颈和技术挑战,需要进行技术尝试和突破。
本文会从底层原理、优化思路、实际场景的优化策略、核心技术实现、优化成果、总结和参考资料进行讲述,期望可以为大家带来一定的启发和帮助,也欢迎多多交流与指正,共建美好的 Flutter 技术社区。
二、渲染机制
原生 vs Flutter
Flutter 本身是基于原生系统之上的,所以渲染机制和 Native 是非常接近的,引用 Google Flutter 团队 Xiao Yu分享[1],如下图所示:
渲染流程
如图左中,Flutter 从接收到 VSync 信号之后整体经历 8 个阶段,其中 Compositing 阶段后会将数据提交给 GPU。
Semantics 阶段会将 RenderObject
marked 需要做语义化更新的信息传递给系统,实现辅助功能,通过语义化接口可以帮助有视力障碍的用户来理解UI内容,和整体绘制流程关联不大。
Finalize Tree 阶段会将所有添加到 _inactiveElements
的不活跃 Element
全部 unmount 掉,和整体绘制流程关联不大。
所以,Flutter 整体渲染流程主要关注 图右 中的阶段:
GPU Vsync
- Flutter Engine 在收到垂直同步信号后,会通知 Flutter Framework 进行 beginFrame,进入 Animation 阶段。
Animation
- 主要执行了
transientCallbacks
回调。
Flutter Engine 会通知 Flutter Framework 进行 drawFrame,进入 Build 阶段
Build
- 构建要呈现的UI组件树的数据结构,即创建对应的
Widget
以及对应的Element
。
Layout
- 目的是要计算出每个节点所占空间的真实大小进行布局;
- 然后更新所有 dirty render objects 的布局信息。
Compositing Bits
- 对需要更新的
RenderObject
进行 update 操作;
Paint
- 生成 Layer Tree,生成 Layer Tree 并不能直接使用,还需要 Compositing 合成为一个 Scene 并进行 Rasterize 光栅化处理。层级合并的原因是因为一般 Flutter 的层级很多,直接把每一层传递给 GPU 效率很低,所以会先做 Composite 提高效率。光栅化之后才会交给 Flutter Engine 处理。
Compositing
- 将 Layout Tree 合成为 Scene,并创建场景当前状态的栅格图像,即进行 Rasterize 光栅化处理,然后提交给 Flutter Engine,最后 Skia 通过 Open GL or Vulkan 接口提交数据给 GPU, GPU经过处理后进行显示。
核心渲染阶段
- Widget
我们平时在写的大都是
Widget
,Widget
其实可以理解为是一个组件树的数据结构,是 Build 阶段的主要部分。其中 Widget Tree 的深度、StatefulWidget
的setState
合理性、build 函数中是否有不合理逻辑以及使用了调用saveLayer
的相关Widget往往会成为性能问题。 - Element
关联
Widget
和RenderObject
,生成Widget
对应的Element
存放上下文信息,Flutter 通过遍历Element
来生成RenderObject
视图树支撑UI结构; - RenderObject
RenderObject 在 Layout 阶段确定布局信息,Paint 阶段生成为对应的 Layer,可见其重要程度。所以 Flutter 中大部分的绘图性能优化发生在这里。RenderObject 树构建的数据会被加入到 Engine 所需的 LayerTree 中。
三、性能优化思路
了解底层渲染机制和核心渲染阶段,可以将优化分为三层:
这里不具体展开讲每一层的优化细节,本文主要从实际的场景来讲述。
四、流式场景
流式组件原理
在原生开发下,通常使用 RecyclerView/UICollectionView
进行列表场景的开发;在Flutter开发下,Flutter Framework 也提供了ListView的组件,它的实质其实是 SliverList
。
核心源码
我们从 SliverList
的核心源码来进行分析:
class SliverList extends SliverMultiBoxAdaptorWidget {
@override
RenderSliverList createRenderObject(BuildContext context) {
final SliverMultiBoxAdaptorElement element = context as SliverMultiBoxAdaptorElement;
return RenderSliverList(childManager: element);
}
}
abstract class SliverMultiBoxAdaptorWidget extends SliverWithKeepAliveWidget {
final SliverChildDelegate delegate;
@override
SliverMultiBoxAdaptorElement createElement() => SliverMultiBoxAdaptorElement(this);
@override
RenderSliverMultiBoxAdaptor createRenderObject(BuildContext context);
}
通过查看 SliverList
的源代码可知,SliverList
是一个 RenderObjectWidget
,结构如下:
我们首先看它的 RenderObject
的核心源码:
class RenderSliverList extends RenderSliverMultiBoxAdaptor {
RenderSliverList({
@required RenderSliverBoxChildManager childManager,
}) : super(childManager: childManager);
@override
void performLayout(){
...
//父节点对子节点的布局限制
final SliverConstraints constraints = this.constraints;
final double scrollOffset = constraints.scrollOffset + constraints.cacheOrigin;
final double remainingExtent = constraints.remainingCacheExtent;
final double targetEndScrollOffset = scrollOffset + remainingExtent;
final BoxConstraints childConstraints = constraints.asBoxConstraints();
...
insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
...
insertAndLayoutChild(childConstraints,after: trailingChildWithLayout,parentUsesSize: true);
...
collectGarbage(leadingGarbage, trailingGarbage);
...
}
}
abstract class RenderSliverMultiBoxAdaptor extends RenderSliver ...{
@protected
RenderBox insertAndLayoutChild(BoxConstraints childConstraints, {@required RenderBox after,...}) {
_createOrObtainChild(index, after: after);
...
}
RenderBox insertAndLayoutLeadingChild(BoxConstraints childConstraints, {@required RenderBox after,...}) {
_createOrObtainChild(index, after: after);
...
}
@protected
void collectGarbage(int leadingGarbage, int trailingGarbage) {
_destroyOrCacheChild(firstChild);
...
}
void _createOrObtainChild(int index, { RenderBox after }) {
_childManager.createChild(index, after: after);
...
}
void _destroyOrCacheChild(RenderBox child) {
if (childParentData.keepAlive) {
//为了更好的性能表现不会进行keepAlive,走else逻辑.
...
} else {
_childManager.removeChild(child);
...
}
}
}
查看 RenderSliverList
的源码发现,对于 child 的创建和移除都是通过其父类 RenderSliverMultiBoxAdaptor
进行。而 RenderSliverMultiBoxAdaptor
是通过 _childManager
即 SliverMultiBoxAdaptorElement
进行的,整个 SliverList
绘制过程中布局大小由父节点给出了限制。
在流式场景下:
- 在滑动过程中是通过
SliverMultiBoxAdaptorElement.createChild
进行对进入可视区新的 child 的创建;(即业务场景的每一个item卡片) - 在滑动过程中是通过
SliverMultiBoxAdaptorElement.removeChild
进行对不在可视区旧的 child 的移除;
我们来看下 SliverMultiBoxAdaptorElement
的核心源码:
class SliverMultiBoxAdaptorElement extends RenderObjectElement implements RenderSliverBoxChildManager {
final SplayTreeMap<int, Element> _childElements = SplayTreeMap<int, Element>();
@override
void createChild(int index, { @required RenderBox after }) {
...
Element newChild = updateChild(_childElements[index], _build(index), index);
...
}
@override
void removeChild(RenderBox child) {
...
final Element result = updateChild(_childElements[index], null, index);
...
}
@override
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
...
final Element newChild = super.updateChild(child, newWidget, newSlot);
...
}
}
通过查看 SliverMultiBoxAdaptorElement
的源码可以发现,对于 child 的操作其实都是通过父类 Element
的 updateChild
进行的。
接下来,我们来看下 Element
的核心代码:
abstract class Element extends DiagnosticableTree implements BuildContext {
@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
if (newWidget == null) {
if (child != null)
deactivateChild(child);
return null;
}
Element newChild;
if (child != null) {
...
bool hasSameSuperclass = oldElementClass == newWidgetClass;;
if (hasSameSuperclass && child.widget == newWidget) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
newChild = child;
} else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget);
newChild = child;
} else {
deactivateChild(child);
newChild = inflateWidget(newWidget, newSlot);
}
} else {
newChild = inflateWidget(newWidget, newSlot);
}
...
return newChild;
}
@protected
Element inflateWidget(Widget newWidget, dynamic newSlot) {
...
final Element newChild = newWidget.createElement();
newChild.mount(this, newSlot);
...
return newChild;
}
@protected
void deactivateChild(Element child) {
child._parent = null;
child.detachRenderObject();
owner._inactiveElements.add(child); // this eventually calls child.deactivate() & child.unmount()
...
}
}
可以看到主要调用 Element
的 mount
和 detachRenderObject
,这里我们来看下 RenderObjectElement
的 这两个方法的源码:
abstract class RenderObjectElement extends Element {
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
...
_renderObject = widget.createRenderObject(this);
attachRenderObject(newSlot);
...
}
@override
void attachRenderObject(dynamic newSlot) {
...
_ancestorRenderObjectElement = _findAncestorRenderObjectElement();
_ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot);
...
}
@override
void detachRenderObject() {
if (_ancestorRenderObjectElement != null) {
_ancestorRenderObjectElement.removeChildRenderObject(renderObject);
_ancestorRenderObjectElement = null;
}
...
}
}
通过查看上面源码的追溯,可知:
在流式场景下:
- 在滑动过程中进入可视区新的 child 的创建,是通过创建全新的
Element
并 mount 挂载到 Element Tree;然后创建对应的RenderObject
,调用了_ancestorRenderObjectElement?.insertChildRenderObject
。 - 在滑动过程中不在可视区旧的 child 的移除,将对应的
Element
从 Element Tree unmount 移除挂载;然后调用了_ancestorRenderObjectElement.removeChildRenderObject
。
其实这个 _ancestorRenderObjectElement
就是 SliverMultiBoxAdaptorElement
,我们再来看下 SliverMultiBoxAdaptorElement
:
class SliverMultiBoxAdaptorElement extends RenderObjectElement implements RenderSliverBoxChildManager {
@override
void insertChildRenderObject(covariant RenderObject child, int slot) {
...
renderObject.insert(child as RenderBox, after: _currentBeforeChild);
...
}
@override
void removeChildRenderObject(covariant RenderObject child) {
...
renderObject.remove(child as RenderBox);
}
}
其实调用的都是 ContainerRenderObjectMixin
的方法,我们再来看下 ContainerRenderObjectMixin
:
mixin ContainerRenderObjectMixin<ChildType extends RenderObject, ... {
void insert(ChildType child, { ChildType after }) {
...
adoptChild(child);// attach render object
_insertIntoChildList(child, after: after);
}
void remove(ChildType child) {
_removeFromChildList(child);
dropChild(child);// detach render object
}
}
ContainerRenderObjectMixin
维护了一个双向链表来持有当前 children RenderObject
,所以在滑动过程中创建和移除都会同步在 ContainerRenderObjectMixin
的双向链表中进行添加和移除。
最后总结下来:
- 在滑动过程中进入可视区新的 child 的创建,是通过创建全新的
Element
并 mount 挂载到 Element Tree;然后创建对应的RenderObject
, 通过调用SliverMultiBoxAdaptorElement.insertChildRenderObject
attach 到 Render Tree,并同步将RenderObject
添加到SliverMultiBoxAdaptorElement
所 mixin 的双链表中; - 在滑动过程中不在可视区旧的 child 的移除,将对应的
Element
从 Element Tree unmount 移除挂载;然后通过用SliverMultiBoxAdaptorElement.removeChildRenderObject
将对应的RenderObject
从所 mixin 的双链表中移除并同步将RenderObject
从 Render Tree detach 掉。
渲染原理
通过核心源码的分析,我们可以对流式场景的 Element
做如下分类:
下面我们来看用户向上滑动查看更多商品卡片并触发加载下一页数据进行展示时,整体的渲染流程和机制:
- 向上滑动时,顶部 0 和 1 的卡片移出 Viewport 区域(Visible Area + Cache Area),我们定义它为进入 Detach Area,进入 Detach Area 后将对应的
RenderObject
从 Render Tree detach 掉,并且将对应的Element
从 Element Tree unmount 移除挂载,并同步从双向链表中移除; - 通过监听
ScrollController
的滑动计算位置来判断是否需要开始加载下一页数据,然后底部 Loading Footer 组件会进入可视区 or 缓存区,需要对SliverChildBuilderDelegate
的 childCount +1,最后一个 child 返回 Loading Footer 组件,同时调用setState
对整个SliverList
刷新。update
会调用performRebuild
进行重构建,中间部分在用户可视区会全部进行 update 操作;然后创建 Loading Footer 组件对应新的Element
和RenderObject
,并同步添加到双向链表中; - 当 loading 结束数据返回后,会再次调用
setState
对整个SliverList
刷新,update
会调用performRebuild
进行重构建,中间部分在用户可视区会全部进行 update 操作;然后将 Loading Footer 组件将对应的RenderObject
从 Render Tree detach 掉,并且将对应的Element
从 Element Tree unmount 移除挂载,并同步从双向链表中移除; - 底部新的 item 会进入可视区 or 缓存区,需要创建对应新的
Element
和RenderObject
,并同步添加到双向链表中;
优化策略
上面用户向上滑动查看更多商品卡片并触发加载下一页数据进行展示的场景,可以从五个方向进行优化:
- Load More
通过监听
ScrollController
的滑动不断进行计算,最好无需判断,自动识别到需要加载下一页数据然后发起 loadMore() 回调。新建ReuseSliverChildBuilderDelegate
增加 loadMore 以及和 item Builder 同级的 footerBuilder,并默认包含 Loading Footer 组件,在SliverMultiBoxAdaptorElement.createChild(int index,...)
判断是否需要动态回调 loadMore() 并自动构建 footer 组件。 - 局部刷新
参考了闲鱼之前在长列表的流畅度优化[2],在下一页数据回来之后调用
setState
对整个SliverList
刷新,导致中间部分在用户可视区会全部进行 update 操作,实际只需刷新新创建的部分,优化SliverMultiBoxAdaptorElement.update(SliverMultiBoxAdaptorWidget newWidget)
的部分实现局部刷新,如下图:
- Element & RenderObject 复用
参考了闲鱼之前在长列表的流畅度优化[2] 和 Google Android RecyclerView ViewHolder 复用设计[3],在有新的 item 创建时,可以做类似 Android
RecyclerView
的ViewHolder
对组件进行持有并复用。基于对渲染机制原理分析,在 Flutter 中Widget
其实可以理解为是一个组件树的数据结构,即更多是组件结构的数据表达。我们需要对移除的 item 的Element
和RenderObject
分组件类型进行缓存持有,在创建新的 item 的时候优先从缓存持有中取出进行复用。同时不破坏 Flutter 本身对Key
的设计,当如果 item 有使用Key
的时候,只复用和它Key
相同的Element
和RenderObject
。但在流式场景列表数据都是不同的数据,所以在流式场景中使用了Key
,也就无法进行任何的复用。如果对Element
和RenderObject
进行复用,item 组件不建议使用Key
。我们在对原有流式场景下
Element
的分类增加一个缓存态:
如下图:
- GC 抑制
Dart 自身有 GC 的机制,类似 Java 的分代回收,可以在滑动的过程中对 GC 进行抑制,定制 GC 回收的算法。针对这项和 Google 的 Flutter 专家讨论,其实 Dart 不像 Java 会存在多线程切换进行垃圾回收的情况,单线程(主isolate)垃圾回收更快更轻量级,同时需要对 Flutter Engine 做深度的改造,考虑收益不大暂不进行。
- 异步化
Flutter Engine 限制非 Main Isolate 调用 Platform 相关 Api,将非跟 Platform Thread 交互的逻辑全部放至新的 isolate 中,频繁
Isolate
的创建和回收也会对性能有一定的影响,Fluttercompute<Q, R>(isolates.ComputeCallback<Q, R> callback, Q message, { String debugLabel })
每次调用会创建新的Isolate
,执行完任务后会进行回收,实现一个类似线程池的Isolate
来进行处理非视图任务。经过实际测试提升不明显,不展开讲述。
核心技术实现
我们可以将调用链路的代码做如下分类:
所有渲染核心在继承自 RenderObjectElement
的 SliverMultiBoxAdaptorElement
中,不破坏原有功能设计以及 Flutter Framework 的结构,新增了 ReuseSliverMultiBoxAdaptorElement
的 Element
来进行优化策略的实现,并且可以直接搭配原有 SliverList
的 RenderSliverList
使用或者自定义的流式组件(例如:瀑布流组件)的 RenderObject
使用。
局部刷新
- 调用链路优化
在 ReuseSliverMultiBoxAdaptorElement
的 update
方法做是否为局部刷新的判断,如果不是局部刷新依然走 performRebuild
;如果是局部刷新,只创建新产生的 item。
- 核心代码
@override
void update(covariant ReuseSliverMultiBoxAdaptorWidget newWidget) {
...
//是否进行局部刷新
if(_isPartialRefresh(oldDelegate, newDelegate)) {
...
Widget newWidget = _buildItem(index);
...
_createChild(index, newWidget);
} else {
// need to rebuild
performRebuild();
}
}
Element & RenderObject 复用
调用链路优化
- 创建
在 ReuseSliverMultiBoxAdaptorElement
的 createChild
方法读取 _cacheElements
对应组件类型缓存的 Element
进行复用;如果没有同类型可复用的 Element
则创建对应新的 Element
和 RenderObject
。
- 移除
在 ReuseSliverMultiBoxAdaptorElement
的 removeChild
方法将移除的 RenderObject
从双链表中移除,不进行 Element
的 deactive 和 RenderObject
的 detach,并将对应的 Element
的 _slot
更新为null,使下次可以正常复用,然后将对应的 Element
缓存到 _cacheElements
对应组件类型的链表中。
注:不 deactive Element
其实不进行调用即可实现,但不 detach RenderObject 无法直接做到,需要在 Flutter Framework 层的 object.dart
文件中,新增一个方法 removeOnly
就是只将 RenderObject
从双链表中移除不进行 detach。
核心代码
- 创建
//新增的方法,createChild会调用到这个方法
_createChild(int index, Widget newWidget){
...
Type delegateChildRuntimeType = _getWidgetRuntimeType(newWidget);
child = _takeChild(delegateChildRuntimeType,index);
...
newChild = updateChild(child, newWidget, index);
...
}
- 移除
@override
void removeChild(RenderBox child) {
...
removeChildRenderObject(child); // call removeOnly
...
removeElement = _childElements.remove(index);
_performCacheElement(removeElement);
}
Load More
- 调用链路优化
在 createChild
时候判断是否是构建 footer 来进行处理。
- 核心代码
@override
void createChild(int index, { @required RenderBox after }) {
...
Widget newWidget;
if(_isBuildFooter(index)){ // call footerBuilder & call onLoadMore
newWidget = _buildFooter();
}else{
newWidget = _buildItem(index);
}
...
_createChild(index, newWidget);
...
}
整体结构设计
- 将核心的优化能力内聚在
Element
层,提供底层能力; - 将
ReuseSliverMultiBoxAdaptorWidget
做为基类默认返回优化后的Element
; - 将 loadMore 和 FooterBuilder 的能力统一由继承自
SliverChildBuilderDelegate
的ReuseSliverChildBuilderDelegate
对上层暴露;
- 如有自己单独定制的流式组件
Widget
,直接把继承关系从RenderObjectWidget
换为ReuseSliverMultiBoxAdaptorWidget
即可,例如自定义的单列表组件(ReuseSliverList)、瀑布流组件(ReuseWaterFall)等。
五、优化成果
基于在之前的一系列深度优化以及切换 Flutter Engine 为UC Hummer 之上,单独控制流式场景的优化变量,使用 PerfDog 获取流畅度数据,进行了流畅度测试对比:
可以看到整体性能数据都有优化提升,结合替换 Engine 之前的测试数据平均来看,对帧率有 2-3 帧的提升,卡顿率下降 1.5 个百分点。
六、总结
使用方式
和原生 SliverList
的使用方式一样,Widget
换成对应可以进行复用的组件 (ReuseSliverList/ReuseWaterFall/ CustomSliverList),delegate 如果需要 footer
和 loadMore
使用 ReuseSliverChildBuilderDelegate
;如果不需要直接使用原生的 SliverChildBuilderDelegate
即可。
- 需要分页场景
return ReuseSliverList( // ReuseWaterFall or CustomSliverList
delegate: ReuseSliverChildBuilderDelegate(
(BuildContext context, int index) {
return getItemWidget(index);
},
//构建footer
footerBuilder: (BuildContext context) {
return DetailMiniFootWidget();
},
//添加loadMore监听
addUnderFlowListener: loadMore,
childCount: dataOfWidgetList.length
)
);
- 无需分页场景
return ReuseSliverList( // ReuseWaterFall or CustomSliverList
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return getItemWidget(index);
},
childCount: dataOfWidgetList.length
)
);
注意点
使用的时候 item/footer 组件不要加 Key
,否则认为只对同 Key
进行复用。因为复用了 Element
,虽然表达组件树数据结果的 Widget
会每次进行更新,但 StatefulElement
的 State
是在 Element
创建的时候生成的,同时也会被复用下来,和 Flutter 本身设计保持一致,所以需要在 didUpdateWidget(covariant T oldWidget)
将 State
缓存的数据重新从 Widget
获取即可。
Reuse Element Lifecycle
将每个 item 的状态进行回调,上层可以做逻辑处理和资源释放等,例如之前在 didUpdateWidget(covariant T oldWidget)
将 State
缓存的数据重新从 Widget
获取可以放置在 onDisappear
里或者自动播放的视频流等;
/// 复用的生命周期
mixin ReuseSliverLifeCycle{
// 前台可见的
void onAppear() {}
// 后台不可见的
void onDisappear() {}
}
七、参考资料
[[1]:Google Flutter 团队 Xiao Yu:Flutter Performance Profiling and Theory](https://files.flutter-io.cn/events/gdd2018/Profiling_your_Flutter_Apps.pdf)
[[2]:闲鱼 云从:他把闲鱼APP长列表流畅度翻了倍](https://mp.weixin.qq.com/s/dlOQ3Hw_U3CFQM91vcTGWQ)
[[3]:Google Android RecyclerView.ViewHolder:RecyclerView.Adapter#onCreateViewHolder](https://developer.android.com/reference/androidx/recyclerview/widget/RecyclerView.Adapter#onCreateViewHolder(android.view.ViewGroup,%20int))