本文原作者: Nayuta,原文发布于: 进击的 Flutter
列表流畅度优化
优化前 |
优化后 |
△ 监控工具来自: fps_monitor
指标详细信息: https://juejin.cn/post/6947911434424549384
流畅: 一帧耗时低于 18ms
良好: 一帧耗时在 18ms-33ms 之间
轻微卡顿: 一帧耗时在 33ms-67ms 之间
卡顿: 一帧耗时大于 66.7ms
采用分帧优化后,卡顿次数从平均 33.3 帧出现了一帧,降低到 200 帧中仅出现了一帧,峰值也从 188ms 降低到 90ms。卡顿现象大幅减轻,流畅帧占比显著提升,整体表现更流畅。下方是详细数据。
优化前 |
优化后 |
|
平均多少帧出现一帧卡顿 | 33.3 |
200 |
平均多少帧出现一帧轻微卡顿 | 8.6 |
66.7 |
最大耗时 | 188.0ms |
90.0ms |
平均耗时 | 27.0ms |
19.4ms |
流畅帧占比 | 40% |
64.5% |
页面切换流畅度提升
如何使用
项目依赖:
dependencies:
version :
非空安全使用: 1.0.2
空安全版本使用: 2.0.2
github 地址: https://github.com/LianjiaTech/keframe
pub 查看: https://pub.dev/packages/keframe
快速上手:
如下图所示
假如现在页面由 A、B、C、D 四部分组成,每部分耗时 10ms,在页面时构建为 40ms。使用分帧组件 FrameSeparateWidget 嵌套每一个部分。页面构建时会在第一帧渲染简单的占位,在后续四帧内分别渲染 A、B、C、D。
对于列表,在每一个 item 中嵌套 FrameSeparateWidget,并将 ListView 嵌套在 SizeCacheWidget 内即可。
构造函数说明
FrameSeparateWidget: 分帧组件,将嵌套的 widget 单独一帧渲染。
类型 |
参数名 |
是否必填 |
含义 |
Key | key | 是 |
|
int | index | 否 |
分帧组件 id,使用 SizeCacheWidget 的场景必传,SizeCacheWidget 中维护了 index 对应的 Size 信息 |
Widget | child | 是 | 实际需要渲染的 widget |
Widget | placeHolder | 否 |
占位 widget,尽量设置简单的占位,不传默认是 Container() |
SizeCacheWidget: 缓存子节点中,分帧组件嵌套的实际 widget 的尺寸信息。
类型 |
参数名 |
是否必填 |
含义 |
Key | key | 否 | |
Widget | child | 是 | 子节点中如果包含分帧组件,则缓存实际的 widget 尺寸 |
int |
estimateCount | 否 |
预估屏幕上子节点的数量,提高快速滚动时的响应速度 |
方案设计与分析
卡顿的本质,就是单帧的绘制时间过长。基于此自然衍生出两种思路解决:
原理并不复杂,问题在于如何在 Flutter 中实践这一机制。
因为涉及到帧与系统的调度,自然联想到看 SchedulerBinding 中有无现成的 API。
发现了 scheduleTask 方法,这是系统提供的一个执行任务的方法,但这个方法存在两个问题:
其中的渲染任务是优先级进行堆排序,而堆排序是不稳定排序,这会导致任务的执行顺序并非 FIFO。从效果上来看,就是列表不会按照顺序渲染,而是会出现跳动渲染的情况。
这个方法本身存在调度问题,我已经提交 issue 与 pr。
最终,参考这个设计结合 endOfFrame 方法的使用,完成了分帧队列。整个渲染流程变为下图所示:
一些展示效果 (Example 说明请查看 Github)
Github
https://github.com/LianjiaTech/keframe/blob/master/README-ZH.md
卡顿的页面往往都是由多个复杂 widget 同时渲染导致。通过为复杂的 widget 嵌套分帧组件 FrameSeparateWidget。渲染时,分帧组件会在第一帧同时渲染多个 palceHolder,之后连续的多帧内依次渲染复杂子项,以此提升页面流畅度。
例如 example 中的优化前示例:
ListView.builder(
itemCount: childCount,
itemBuilder: (c, i) => CellWidget(
color: i % 2 == 0 ? Colors.red : Colors.blue,
index: i,
),
)
其中 CellWidget 高度为 60,内部嵌套了三个 TextField 的组件 (整体构建耗时在 9ms 左右)。
优化仅需为每一个 item 嵌套分帧组件,并为其设置 placeHolder (placeHolder 尽量简单,样式与实际 item 接近即可)。
在列表情况下,给 ListView 嵌套 SizeCacheWidget,同时建议将预加载范围 cacheExtent 设置大一点,例如 500 (该属性默认为 250),提升慢速滑动时候的体验。
此外,也可以给 item 嵌套透明度/位移等动画,优化视觉上的效果。
效果如下图:
分帧的成本
当然分帧方案也非十全十美,在我看来主要有两点成本:
优化前后对比演示
优化前 | 优化后 |
最后: 一点点思考
长按右侧二维码
查看更多开发者精彩分享
"开发者说·DTalk" 面向中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。
点击屏末 | 阅读原文 | 即刻报名参与 "开发者说·DTalk"