前言
卡顿的产生
为什么一帧的耗时会超过16.7ms?为了搞清楚这个问题我们需要知道,Flutter为了绘制一帧会做些什么?
绘制过程分析:首先 dart 通过 Window_scheduleFrame 方法调用 engine,之后 engine 向 Choreographer 注册一个 vsync 回调。等到下一个 vsync 信号来到时,通过 nativeOnVsync 将整个渲染任务 post 到 UI task 的队列中,回调到 Flutter之后经历以下流程:

关键步骤有三:
Build:通过widget 配置生成 Element 与 RenderObject 树
Layout:遍历 RenderObject 树,测量每一个页面元素的大小与决定位置
Paint:根据 RenderObject 树中的节点生成 Layer 树,合成语义化后提交给 engine 给 GPU 进行渲染
对于复杂列表卡顿问题主要由于 build 阶段耗时过长。

Flutter 的列表一般都采用 Lazy Build 的方式生成列表单元,当列表单元接近可见区域的时候,列表根据视窗高度与缓存区大小,动态构建和布局多个 item。
不管是对于列表还是非列表而言,卡顿大多由于构建耗时引起。从本质来看,就是一个模块的执行时间过长。
Keframe作为flutter的一个流畅度解决方案,已受到业界很多关注。
Keframe 原理是基于分帧渲染,针对单一计算量过大以及复杂列表的滚动,提升效果非常明显。
在 Flutter 中,Widget/Element/Render 三棵树的概念非常重要,而分帧渲染的原理,其实就是在 Tree 上分层,将一些复杂的节点及其子节点,用一些空 Widget 占位,而原本应该被渲染的节点,放在下一帧去渲染,从而避免出现太复杂的 UI,使得一帧的单帧的渲染时间过长,导致卡顿。
这里给出个流畅度指标信息:
1.流畅:一帧耗时低于 18ms
2.良好:一帧耗时在 18ms-33ms 之间
3.轻微卡顿:一帧耗时在 33ms-67ms 之间
4.卡顿:一帧耗时大于 66.7ms
如何使用
在 pubspec.yaml 中添加 keframe 依赖
keframe: 2.0.2
版本说明:
非空安全使用:1.0.1;
空安全版本使用:2.0.2;
方案设计与分析:
如图所示:

根据图示,原理清晰明了,问题是flutter如何实现这一过程。
源码分析
分帧上屏

initState 初始化时 resultWidget 赋值为占位Widget
transformWidget initState和didUpdate都会触发,监听占位绘制完成,并将替换任务扔进分帧队列中
分帧队列

await SchedulerBinding.instance!.endOfFrame : 如果当前正在绘制,等待当前帧结束。如果当前空闲,强制进行一帧的绘制,并等待结束。
await taskItemQueue.first.run() : 该方法为callback回调,真实内容就是替换占位widget
SizeCacheWidget
//自定义冒泡通知
class LayoutInfoNotification extends Notification {
final Size size;
final int? index;
LayoutInfoNotification(this.index, this.size);
}


子组件重写performLayout方法,将尺寸大小通过冒泡通知给父组件,父组件根据id进行存储。
分帧的成本
当然分帧方案也非十全十美,在我看来主要有两点成本:
1、额外的构建开销:整个构建过程的构建消耗由「n * widget消耗 」变成了「n *( widget + 占位)消耗 + 系统调度 n 帧消耗」。可以看出,额外的开销主要由占位的复杂度决定。如果占位只是简单的 Container,。这种额外开销对于当下的移动设备而言,成本几乎可以不计。
2、视觉上的变化:组件会将 item 分帧渲染,页面在视觉上出现占位变成实际 widget 的过程。但其实由于列表存在缓存区域(建议将缓存区调大),在高端机或正常滑动情况下用户并无感知。而在中低端设备上快速滑动能感觉到切换的过程,但比严重顿挫要好。