已开源!Flutter 流畅度优化组件 keframe

  • Post author:
  • Post category:其他




列表流畅度优化

这是一个通用的流畅度优化方案,通过分帧渲染优化由构建导致的卡顿,例如页面切换或者复杂列表快速滚动的场景。

代码中 example 运行在 VIVO X23(骁龙 660),在相同的滚动操作下优化前后 200 帧采集数据指标对比(录屏在文章最后):

优化前 优化后
优化前 优化后

监控工具来自:

fps_monitor

,指标详细信息:

页面流畅度不再是谜!调试神器开箱即用,Flutter FPS检测工具

  • 流畅:一帧耗时低于 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

,整体切换过程更加流畅。

image.png image.png



如何使用?



项目依赖:



pubspec.yaml

中添加

keframe

依赖

dependencies:
  keframe: version

组件仅区分非空安全与空安全版本

非空安全使用:

1.0.1

空安全版本使用:

2.0.1

github 地址:

https://github.com/LianjiaTech/keframe

pub 查看:

https://pub.dev/packages/keframe


Dont forget star ~



快速上手:

如下图所示

image.png

假如现在页面由 A、B、C、D 四部分组成,每部分耗时 10ms,在页面时构建为 40ms。使用分帧组件

FrameSeparateWidget

嵌套每一个部分。页面构建时会在第一帧渲染简单的占位,在后续四帧内分别渲染 A、B、C、D。

对于列表,在每一个 item 中嵌套

FrameSeparateWidget

,并将

ListView

嵌套在

SizeCacheWidget

内即可。

image.png




构造函数说明

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 预估屏幕上子节点的数量,提高快速滚动时的响应速度



方案设计与分析:

卡顿的本质,就是

单帧的绘制时间过长

。基于此自然衍生出两种思路解决:

1、减少一帧的绘制耗时,因为导致耗时过长的原因有很多,比如不合理的刷新,或者绘制时间过长,都有可能,需要具体问题具体分析,后面我会分享一些我的优化经验。


2、在不对耗时优化下,将一帧的任务拆分到多帧内,保证每一帧都不超时。这也是本组件的设计思路,分帧渲染。

如下图所示:

image.png

原理并不复杂,问题在于如何在 Flutter 中实践这一机制。

因为涉及到帧与系统的调度,自然联想到看

SchedulerBinding

中有无现成的 API。

发现了

scheduleTask

方法,这是系统提供的一个执行任务的方法,但这个方法存在两个问题:

  • 1、其中的渲染任务是优先级进行堆排序,而堆排序是

    不稳定

    排序,这会导致任务的执行顺序并非 FIFO。从效果上来看,就是列表不会按照顺序渲染,而是会出现跳动渲染的情况

  • 2、这个方法本身存在调度问题,我已经提交 issue 与 pr,不过一直卡在单元测试上,如果感兴趣可以以在这里交流谈论。


fix: Tasks scheduled through ‘SchedulerBinding.instance.scheduleTask’… #82781

最终,参考这个设计结合

endOfFrame

方法的使用,完成了分帧队列。整个渲染流程变为下图所示:

image.png

对于列表构建场景来说,假设屏幕上能显示五个 item。首先在第一帧的时候,列表会渲染 5 个占位的 Widget,同时添加 5 个高优先级任务到队列中,这里的任务可以是简单的将占位 Widget 和实际 item进行替换,也可通过渐变等动画提升体验。在后续的五帧中占位 Widget 依次被替换成实际的列表 item。



ListView流畅度翻倍!!Flutter卡顿分析和通用优化方案

这篇文章中有更加详细的分析。




一些展示效果(Example 说明请查看

Github


卡顿的页面往往都是由多个复杂 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),提升慢速滑动时候的体验。

Screenrecording_20210611_194905.gif
(占位与实际列表项不一致时,首次渲染抖动,二次渲染正常)

此外,也可以给 item 嵌套透明度/位移等动画,优化视觉上的效果。

效果如下图:

Screenrecording_20210315_133310.gif Screenrecording_20210315_133848.gif



分帧的成本

当然分帧方案也非十全十美,在我看来主要有两点成本:

1、额外的构建开销:整个构建过程的构建消耗由「n * widget消耗 」变成了「n *( widget + 占位)消耗 + 系统调度 n 帧消耗」。可以看出,额外的开销主要由占位的复杂度决定。如果占位只是简单的 Container,测试后发现整体构建耗时大概提升在 15 % 左右。这种额外开销对于当下的移动设备而言,成本几乎可以不计。

2、视觉上的变化:如同上面的演示,组件会将 item 分帧渲染,页面在视觉上出现占位变成实际 widget 的过程。但其实由于列表存在缓存区域(建议将缓存区调大),在高端机或正常滑动情况下用户并无感知。而在中低端设备上快速滑动能感觉到切换的过程,但比严重顿挫要好。




优化前后对比演示

注:gif 帧率只有20

优化前 优化后
优化前 优化后



最后:一点点思考

列表优化篇到此告一段落,在整个开源实践过程中,有两点感触较深:



「点」与「面」的关系

我们在思考技术方案的时候可以由「点」到「面」,站在一个较高视野去想问题的本质。

而在执行的时候则需要由「面」到「点」的进行逐级拆分,抓住问题的关键节点,并且拟定进度计划,逐步破解。

很多时候,这种

向上和向下的逻辑思维才是我们的核心竞争力



以不变应万变

对于未知的东西,我们往往会过度的将它想复杂。在一开始分析列表构建原理的时候,我也苦于无从下手,走了很多弯路。但其实对于 Flutter 这套

「UI」

框架而言,核心仍然在于

三棵树的构建机制

在这套体系内,抓住

不变

的东西,无论是生命周期、路由等等问题都可以从里面找到答案。我之前也有过总结:

Flutter 核心渲染机制



Flutter路由设计与源码解析

下一阶段,我会聚焦于 Dart 中的 I/O 部分,结合计算机网络原理由浅入深地进行分析与实践。从底层原理出发,与大家一起学习

「不变的原理」

,一起进步。如果你有任何疑问可以通过公众号与联系我,如果文章对你有所启发,希望能得到你的点赞、关注和收藏,这是我持续写作的最大动力。Thanks~

如果遇到了任何问题与建议,欢迎在评论区或者公众号联系我,或者 issue 和 pr。

公众号:

进击的Flutter

或者

runflutter

里面整理收集了最详细的Flutter进阶与优化指南,欢迎关注。



版权声明:本文为Nayuta403原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。