淘特 Flutter 流畅度优化实践 · 二期


作者:谢伟(韦圣)
在上一篇《淘特 Flutter 流畅度优化实践》中说到,虽然一期效果较为明显,但距离极致的用户体验仍有不小的差距。去年,淘特端架构联合业务团队共同发起“基础链路极致体验优化”的项目,目标在时长与流畅度方面获得极致体验,本文将为大家详细解析淘特 Flutter 流畅度优化实践二期部分。
优化效果
首先,我们简单回顾上一期优化后的效果, 在一期中,主要的优化措施集中在“业务最佳实践”,不需要改 Engine、不需要造轮子,依然取得不错的优化效果。证明 Flutter 在面对复杂的业务场景时, 只要掌握好足够的实践经验,依然能保持较好的性能表现。
但随着淘特的优化进入深水区,源自对极致用户体验的追求及 Native 性能数据的赛跑,让我们开始快速从集团、业界吸收优秀前辈的经验并自我突破。最终,在完成了“Hummer 引擎升级”、“自研 ExternalImage 图片库”、“自研 FlowView 高性能流式容器”等多项重点技术突破后,取得的二期成果如下:

注:流畅度随着业务迭代、测试口径的变化存在一定波动。以上数据来自:淘特 4.14.0 双端慢滑测试口径(滑动速率参考下方录屏)
从体感录屏来看有两大提升:
流畅度再度提升,双端慢滑基本无卡顿
消灭 iOS 大卡顿问题
Android 录屏对比(左边优化前、右边优化后)
查看视频请点击:淘特 Flutter 流畅度优化实践 · 二期
iOS 录屏对比(左边优化前、右边优化后)
查看视频请点击:淘特 Flutter 流畅度优化实践 · 二期
优化过程中关键技术
引擎升级 Hummer
相信不少同学都听过 Hummer 引擎(UC Flutter 定制引擎),得益于 UC 丰富的渲染性能优化经验及 AliFlutter 社区生态,淘特在充分调研后决定接入 Hummer,但同时接入的过程中,淘特又与 Hummer 遇见了很多复杂场景需要解决的流畅度、加载耗时、内存等各方面的问题。
首先升级 Hummer 引擎带来的流畅度优化主要在以下 4 个方面,具体原理参考 Hummer 引擎介绍,本文侧重讲淘特实际解决的问题与解法,及升级的对比效果。

下面是引擎升级的最终效果:
淘特 Android4.4.0 基线


淘特 iOS4.6.0 基线


双端在帧率、卡顿率都有了明显的提升,尤其是 iOS 端解决了 Feeds 流在 iPhone6 下容易大卡顿的问题。而升级引擎对淘特影响最大的就是外接图片库的升级。
Flutter 图片库对比
以流畅度为例, 图片库的选择是至关重要的。由于 Hummer 引擎中迁移原有 CDNImage(旧引擎耦合 Engine 的外接图片库方案)的成本过大,这迫使淘特寻找新的“非侵入引擎的图片库方案”。
最开始我们尝试用 NetworkImage 拿到了第一手的体验数据。流畅度提升非常明显,但 NetworkImage 的不足只能满足短期快速灰度,此时 FImage(外接纹理的图片库方案)出现在我们面前,无疑是一个非常高效的解决方案,但实测后发现低端机流畅度较之前略有下滑,这让我们开始探寻更适合淘特的图片库方案。
最终自研 ExternalImage, 在流畅度和加载耗时对比中均取得超越 CDNImage 的效果。
以下是集团 Flutter 主要的图片库方案对比:

以下为 Hummer 下 ExternalImage 与外接纹理方案帧耗时性能图,我们发现外接纹理方案的 Raster 线程帧耗时会显著高于原生 Image 组件方案,原因初步分析是过多的 Texture 使低端机 Raster 耗时负载过大, 而由于 frame_time 近似等于 max(ui,raster), 所以当 raster 帧耗时超越 16ms 也会造成实际体感 FPS 的降低。于是就有了下一节的 ExternalImage 图片库。

淘特 ExternalImage 图片库

上面是一张 ExternalImage 的总体架构图, 其基于 FFI 方式加载来自 Native 的像素数据,从官方 Image 组件出发,经 Provider 发起 Channel 调用,拿到图片返回结果后触发 decode、setState 等流程, 绿色代表请求链路,黄色代表回传链路。
核心技术点如下:

淘特 Flow-View 轻量级流式容器
列表滚动一直是 Flutter 流畅度优化的重点场景,在未获得 Hummer 引擎优化前, 流畅度优化一度遇到瓶颈。在 Flutter 官方流式容器设计理念中,在列表滚动时, Element 被视为非常轻量级的组件,没有支持复用。在列表增减元素时,Widget 被认为非常轻量, 未做局部刷新。这在淘特实际复杂的 Feeds 业务场景下,受到了严峻挑战。我们结合业务特性,自研了一套轻量级流式容器方案 Flow-View。 主要支持 2 个特性:
局部刷新, 将使分页加载更多无需重复 build 已有的 itemWidget;
Element/RenderObject 复用, 将使滚动插入新元素时效率更高。
FlowView 局部刷新

我们首先看上图右侧示意图,在局部刷新场景下,左边未优化前添加新元素将 setState 触发整个列表 Rebuild。右边优化后将只对新增的 2 个元素执行插入操作,已有的元素无需 Rebuild。
左侧为源码细节,通过在 SliverMultiBoxAdaptorElement.update 方法中,通过滚动到底部,且 newDelegate.childCount>oldDelegate 判断为加载更多场景执行局部刷新(即插入新的元素)。
FlowView Element、RenderObject 复用

同样先看上图右侧示意图,在滚动场景下,当新的元素 12、13 即将入屏时, 左边未优化前将创建全新的 Element、RenderObject。右边优化后新元素 12、13 将复用顶部移出的 0、1 元素的 Element、RenderObject。做到循环利用,效率更高。
左侧为源码细节,当获取新插入的 item 时,通过在 SliverMultiBoxAdaptorElement.createChild 方法中,未优化前_childElements[index]=null 将触发 Element、RenderObjec 新建,优化后将先根据新元素的类型找是否有可复用的元素,再触发 updateChild, 若缓存不为空,则会执行 didUpdateWidget 逻辑。
当移除元素时,removeChild 将不再 deactive Element(即不触发 updateChild)。同时通过修改 framework 将 RenderObject 从 ContainerRenderObjectMixin 双向链表中移除(renderObject._removeFromChildList)。再将 Element 添加进 cacheMap 即可。
Android 滑动手感
在淘特,不仅关注流畅度数据的提升,更关注用户实际的体感。在某一次版本升级后,Android 的手感不如以前顺滑,滑动初期阻尼感升高明显。如下视频对比。
1.优化前 4.2.0
查看视频请点击:淘特 Flutter 流畅度优化实践 · 二期
2.优化后 4.3.0
查看视频请点击:淘特 Flutter 流畅度优化实践 · 二期
分析原因是 基于 BouncingScrollSimulation 实现的下拉刷新组件改变了 Android 平台原有的 Simulation。经过分析 Android 和 iOS 平台的 Simulation 滑动算法。

我们决定在 Android 上根据是否滚动到尽头时区分 Simulation 算法。在未滚动到底部前,我们仍然用 ClampingScrollSimulation 保持近似 Android 原生的手感,滚动到尽头后为支持下拉刷新,切换至 ScrollSpringSimulation。基于此动态切换的算法封装了一个通用的 BouncingableClampingScrollSimulation 供业务使用。

总结与展望

综上,在一期、二期的优化中,淘特 Flutter 流畅度优化在线下测试中取得了不错的效果,部分页面超过了 Native。低端机也稳定在较高的帧率。但线上用户的真实场景远比线下复杂的多,所以淘特将在今年对以下三个方面做加大投入。
从线下走向线上,参与 AliFlutter-APM 共建、建设淘特全链路性能分析平台。
在机型方面,淘特在从之前重点关注的低端机体验优化走向全机型的体验优化,这块计划在如滑动插值器卡顿调优、 适配 iphone 高刷屏、升级 FlutterImageView/SurfaceTexture 发力。
最后, 之前的优化工作很大一部分是人工专项优化,后续将做到更多流程自动化,如在低性能 Widget 告警工具、集成性能卡口工具等方面发力, 保障业务高质量低成本保持优化成果。
【参考文献】
[1]:Google Flutter 团队 Xiao Yu:Flutter Performance Profiling and Theory
[3]:Google Android RecyclerView.ViewHolder:RecyclerView.Adapter#onCreateViewHolder
[4]: Jank卡顿及stutter卡顿率说明
版权声明: 本文为 InfoQ 作者【阿里巴巴移动技术】的原创文章。
原文链接:【http://xie.infoq.cn/article/86e2fa3147186e5f04dfa3043】。文章转载请联系作者。
评论