vlambda博客
学习文章列表

已开源!Flutter 基于分帧渲染的流畅度优化组件 Keframe

大家好,这里是承香墨影!

Keframe 原理是基于分帧渲染,针对页面切换以及复杂列表的滚动,提升效果非常明显。

在 Flutter 中,Widget/Element/Render 三棵树的概念非常重要,而分帧渲染的原理,其实就是在 Tree 上分层,将一些复杂的节点及其子节点,用一些空 Widget 占位,而原本应该被渲染的节点,放在下一帧去渲染,从而避免出现太复杂的 UI,使得一帧的渲染超过 16.6ms,导致卡顿。

Keframe 的代码量不大,但实现原理值得研究。接下来正式介绍 Keframe,一起看看它的使用以及各项数据的表现。

列表流畅度优化

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

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

优化前 优化后
已开源!Flutter 基于分帧渲染的流畅度优化组件 Keframe 已开源!Flutter 基于分帧渲染的流畅度优化组件 Keframe

监控数据来自:fps_monitor

  • 流畅:一帧耗时低于 18ms;
  • 良好:一帧耗时在 18ms-33ms 之间;
  • 轻微卡顿:一帧耗时在 33ms-67ms 之间;
  • 卡顿:一帧耗时大于 66.7ms;

fps_monitor: https://github.com/Nayuta403/fps_monitor

采用分帧优化后,卡顿次数从 平均 33.3 帧出现了一帧,降低到 200 帧中仅出现了一帧,峰值也从 188ms 降低到 90ms。卡顿现象大幅减轻,流畅帧占比显著提升,整体表现更流畅。

下方是详细数据。

已开源!Flutter 基于分帧渲染的流畅度优化组件 Keframe

页面切换流畅度提升

在打开一个页面或者 Tab 切换时,系统会渲染整个页面并结合动画完成页面切换。对于复杂页面,同样会出现卡顿掉帧。

借助分帧组件,将页面的构建逐帧拆解,通过 DevTools 中的性能工具查看。切换过程的峰值由 112.5ms 降低到 30.2 ms,整体切换过程更加流畅。

已开源!Flutter 基于分帧渲染的流畅度优化组件 Keframe
已开源!Flutter 基于分帧渲染的流畅度优化组件 Keframe

如何使用?

项目依赖:

pubspec.yaml 中添加 keframe 依赖

dependencies: 
  keframe: version

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

  • 非空安全使用: 1.0.1
  • 空安全版本使用: 2.0.0

Package:https://pub.dev/packages/keframe

快速上手:

如下图所示

已开源!Flutter 基于分帧渲染的流畅度优化组件 Keframe

假如现在页面由 A、B、C、D 四部分组成,每部分耗时 10ms,在页面时构建为 40ms。使用分帧组件 FrameSeparateWidget 嵌套每一个部分。页面构建时会在第一帧渲染简单的占位,在后续四帧内分别渲染 A、B、C、D。

对于列表,在每一个 item 中嵌套 FrameSeparateWidget,并将 ListView 嵌套在 SizeCacheWidget 内即可。

已开源!Flutter 基于分帧渲染的流畅度优化组件 Keframe

构造函数说明

FrameSeparateWidget :分帧组件,将嵌套的 widget 单独一帧渲染。

已开源!Flutter 基于分帧渲染的流畅度优化组件 Keframe

SizeCacheWidget:缓存子节点中,分帧组件嵌套的实际 widget 的尺寸信息。

已开源!Flutter 基于分帧渲染的流畅度优化组件 Keframe

Example 示例说明:

卡顿的页面往往都是由多个复杂 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 嵌套分帧组件,并为其设置 placeHolderplaceHolder 尽量简单,样式与实际 item 接近即可)。

在列表情况下,给 ListView 嵌套 SizeCacheWidget,同时建议将预加载范围 cacheExtent 设置大一点,例如 500(该属性默认为 250),提升慢速滑动时候的体验。

例如:

SizeCacheWidget(
  child: ListView.builder(
    cacheExtent: 500,
    itemCount: childCount,
    itemBuilder: (c, i) => FrameSeparateWidget(
      index: i,
      placeHolder: Container(
        color: i % 2 == 0 ? Colors.red : Colors.blue,
        height: 60,
      ),
      child: CellWidget(
        color: i % 2 == 0 ? Colors.red : Colors.blue,
        index: i,
      ),
    ),
  ),
),

下面是几种场景说明:

1、列表中实际 item 尺寸已知的情况

实际 item 高度已知的情况下(每个 item 高度为 60),将占位设置与实际 item 高度一致即可,查看 example 中分帧优化1。

FrameSeparateWidget(
  index: i,
  placeHolder: Container(
    color: i % 2 == 0 ? Colors.red : Colors.blue,
    height: 60,// 与实际 item 高度保持一致
  ),
  child: CellWidget(
    color: i % 2 == 0 ? Colors.red : Colors.blue,
    index: i,
  ),
)

2、列表中实际 item 高度未知的情况

现实场景中,列表往往是根据数据下发展示,无法一开始预知 item 的尺寸。

例如,example 中 分帧优化 2, placeHolder高度40)与实际 item (高度60)尺寸不一致, 由于每一个 item 分在不同帧完成渲染,因此会出现列表「抖动」的情况。

这时可以给 placeholder 设置一个近似的高度。并且在将 ListView 嵌套在 SizeCacheWidget 中。对于已渲染过的 widget 会强制设置 palceHolder 的尺寸,同时将 cacheExtent调大。这样在来回滑动过程中,已经渲染过的 item 将不会出现跳动情况。

例如,example 中「分帧优化 3」。

SizeCacheWidget(
  child: ListView.builder(
    cacheExtent: 500,
    itemCount: childCount,
    itemBuilder: (c, i) => FrameSeparateWidget(
      index: i,
      placeHolder: Container(
        color: i % 2 == 0 ? Colors.red : Colors.blue,
        height: 40,
      ),
      child: CellWidget(
        color: i % 2 == 0 ? Colors.red : Colors.blue,
        index: i,
      ),
    ),
  ),
),

实际效果如下:

已开源!Flutter 基于分帧渲染的流畅度优化组件 Keframe

3、预估一屏 item 的数量

如果能粗略估计一屏能展示的实际 item 的最大数量,例如 10。将 SizeCacheWidget 的 estimateCount 属性设置为 10*2。快速滚动场景构建响应更快,并且内存更稳定。例如,example 中的「分帧优化4」。

SizeCacheWidget(
    estimateCount: 20,
    child: ListView.builder(

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

已开源!Flutter 基于分帧渲染的流畅度优化组件 Keframe

已开源!Flutter 基于分帧渲染的流畅度优化组件 Keframe

4、非列表场景

对于非列表场景,一般不存在流畅度问题,不过在初次进入的时候任然可能出现卡顿。同样的,可以将复杂的模块分到不同帧渲染,避免初次进入的卡顿。例如,我们将为优化例子中底部的操作区域嵌套分帧组件:

FrameSeparateWidget(
    child: operateBar(),
    index: -1,
)

分帧的成本

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

首先,额外的构建开销:整个构建过程的构建消耗由「n * widget消耗 」变成了「n *( widget + 占位)消耗 + 系统调度 n 帧消耗」。

可以看出,额外的开销主要由占位的复杂度决定。如果占位只是简单的 Container,测试后发现整体构建耗时大概提升在 15% 左右。

这种额外开销对于当下的移动设备而言,成本几乎可以不计。

其次,视觉上的变化:如同上面的演示,组件会将 item 分帧渲染,页面在视觉上出现占位变成实际 widget 的过程。

但其实由于列表存在缓存区域(建议将缓存区调大),在高端机或正常滑动情况下用户并无感知。而在中低端设备上快速滑动能感觉到切换的过程,但比严重顿挫要好。

优化前后对比演示

注:gif 帧率只有20。

已开源!Flutter 基于分帧渲染的流畅度优化组件 Keframe

已开源!Flutter 基于分帧渲染的流畅度优化组件 Keframe

最后:一点点思考

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

「点」与「面」的关系

我们在思考技术方案的时候可以由「点」到「面」,从一个较高的维度思考问题本质。而在执行的时候则需要由「面」到「点」的进行逐级拆分,抓住问题的关键节点,并且拟定进度计划,逐步破解。很多时候,这种向上和向下的逻辑思维才是我们的核心竞争力。

以不变应万变

对于未知的东西,我们往往会过度的将它想复杂。在一开始分析列表构建原理的时候,我也苦于无从下手,走了很多弯路。但其实对于 Flutter 这套 「UI」 框架而言,核心仍然在于三棵树的构建机制。在这套体系内,抓住不变的东西,无论是生命周期、路由等等问题都可以从里面找到答案。

Keframe 已开源,欢迎点击「阅读原文」跳转至 Github fork star。
承香墨影
我是承香墨影,8 年技术老司机。在这里,主要分享我个人的原创内容,不仅限于技术,职场、产品、设计思想等等,统统都有。这里已经汇集了有很多小伙伴了,欢迎你加入!
328篇原创内容
Official Account

-- End --

本文对你有帮助吗?留言、转发、点好看是最大的支持,谢谢!

推荐阅读: