已开源!Flutter 流畅度优化组件 Keframe | 开发者说·DTalk
Posted 谷歌开发者
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了已开源!Flutter 流畅度优化组件 Keframe | 开发者说·DTalk相关的知识,希望对你有一定的参考价值。
本文原作者: Nayuta,原文发布于: 进击的 Flutter
列表流畅度优化
这是一个通用的流畅度优化方案,通过分帧渲染优化由构建导致的卡顿,例如页面切换或者复杂列表快速滚动的场景。
代码中 example 运行在 VIVO X23 (骁龙 660),在相同的滚动操作下优化前后 200 帧采集数据指标对比 (录屏在文章最后):
优化前 | 优化后 |
△ 监控工具来自: 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% |
页面切换流畅度提升
在打开一个页面或者 Tab 切换时,系统会渲染整个页面并结合动画完成页面切换。对于复杂页面,同样会出现卡顿掉帧。借助分帧组件,将页面的构建逐帧拆解,通过 DevTools 中的性能工具查看。切换过程的峰值由 112.5ms 降低到 30.2 ms,整体切换过程更加流畅。
如何使用
项目依赖:
在 pubspec.yaml 中添加 keframe 依赖:
dependencies:
keframe: 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 方法的使用,完成了分帧队列。整个渲染流程变为下图所示:
对于列表构建场景来说,假设屏幕上能显示五个 item。首先在第一帧的时候,列表会渲染 5 个占位的 Widget,同时添加 5 个高优先级任务到队列中,这里的任务可以是简单的将占位 Widget 和实际 item 进行替换,也可通过渐变等动画提升体验。在后续的五帧中占位 Widget 依次被替换成实际的列表 item。
一些展示效果 (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 嵌套透明度/位移等动画,优化视觉上的效果。
效果如下图:
分帧的成本
当然分帧方案也非十全十美,在我看来主要有两点成本:
额外的构建开销: 整个构建过程的构建消耗由「n * widget消耗 」变成了「n * (widget + 占位) 消耗 + 系统调度 n 帧消耗」。可以看出,额外的开销主要由占位的复杂度决定。如果占位只是简单的 Container,测试后发现整体构建耗时大概提升在 15 % 左右。这种额外开销对于当下的移动设备而言,成本几乎可以不计。
视觉上的变化: 如同上面的演示,组件会将 item 分帧渲染,页面在视觉上出现占位变成实际 widget 的过程。但其实由于列表存在缓存区域 (建议将缓存区调大),在高端机或正常滑动情况下用户并无感知。而在中低端设备上快速滑动能感觉到切换的过程,但比严重顿挫要好。
优化前后对比演示
注: gif 帧率只有 20
优化前 | 优化后 |
最后: 一点点思考
列表优化篇到此告一段落,在整个开源实践过程中,有两点感触较深:
「点」与「面」的关系
我们在思考技术方案的时候可以由「点」到「面」,站在一个较高视野去想问题的本质。
而在执行的时候则需要由「面」到「点」的进行逐级拆分,抓住问题的关键节点,并且拟定进度计划,逐步破解。
很多时候,这种向上和向下的逻辑思维才是我们的核心竞争力。
以不变应万变
对于未知的东西,我们往往会过度的将它想复杂。在一开始分析列表构建原理的时候,我也苦于无从下手,走了很多弯路。但其实对于 Flutter 这套「UI」框架而言,核心仍然在于三棵树的构建机制。
在这套体系内,抓住不变的东西,无论是生命周期、路由等等问题都可以从里面找到答案。
长按右侧二维码
查看更多开发者精彩分享
"开发者说·DTalk" 面向中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。
点击屏末 | 阅读原文 | 即刻报名参与 "开发者说·DTalk"
以上是关于已开源!Flutter 流畅度优化组件 Keframe | 开发者说·DTalk的主要内容,如果未能解决你的问题,请参考以下文章