写点什么

淘特 Flutter 流畅度优化实践

  • 2021 年 12 月 27 日
  • 本文字数:5719 字

    阅读完需:约 19 分钟

淘特 Flutter 流畅度优化实践


作者:谢伟(韦圣)


不同的业务背景引出不同的技术诉求,“用户体验特爽”是淘特的不懈追求,本文将介绍笔者加入淘特以来在 Flutter 流畅度方面的诸多优化实践,这些优化不涉及 Engine 改造、不涉及高大上的“轮子建设“,只需细心细致深入业务抽丝剥茧,坚持实际体感导向,即能为用户体验带来显著提升,值得 Flutter 开发者将其应用在产品的每一个像素。

背景

淘特具备鲜明的三大特征:


  1. 业务特征:淘特拥有业界最复杂的淘系电商链路

  2. 用户特征:淘特用户中有大量的中老年用户,大量的用户手机系统版本较低,大量的用户使用中低端机

  3. 技术特征:淘特大规模采用 Flutter 跨平台渲染技术


综上所述:


最复杂业务链路+最低性能用户群体+最新的跨平台技术==>核心问题之一:页面流畅度受到严峻挑战

注:相关数据以 vivo Y67,淘特

3.32.999.10 (103) 测得

目标

流畅度是用户体验的关键一环,大家都不希望手机用起来像看电影/刷 PPT,尤其是现在高刷屏(90/120hz)的普及,更是极大强化了用户对流畅度的感知,但流畅度也跟产品复杂度强相关,也是一次繁与简的取舍,淘特流畅度一期优化目标:


Flutter 核心链路页面达到高流畅度(平均帧率:低端机 45FPS、中端机 50FPS、高端 50FPS)

一期优化后的状态



旧版 3.32 如视频左,新版 3.37 如视频右。因 uiautomator 工具会触发无障碍性能 ISSUE,此版本对比为人工测试。


视频请见:淘特 Flutter 流畅度优化实践


除了数据上的明显提升,体感上,旧版快滑卡顿明显,画面突变明显,新版则基本消除明显的卡顿,画面连续平稳。

问题

回到技术本身,Flutter 为什么会卡顿、帧率低?总的来说均为以下 2 个原因:


  1. UI 线程慢了-->渲染指令出的慢

  2. GPU 线程慢了-->光栅化慢、图层合成慢、像素上屏慢


那么,怎么解上述的 2 个问题是咱们所关心的重点。既然知道某块有问题,我们自然要有工具系统化的度量问题水平,以及系统化的理论支撑实践,且看以下 2 节,过程中穿插相关策略在淘特的实践, 理论与实践结合理解更透。

怎么解

解法 - 案例

降低 setState 的触发节点

大家都知道 Flutter 的刷新机制,在越高的 Widget 树层级触发 setState 标脏 Element,“脏树越大”,在越低层级越局部的 Widget 触发状态更新,“脏树越小”,被标记为脏树后将触发 Element.Rebuild,遍历组件树。原理请看下图“Flutter 页面刷新机制源码解析”:





“Element.updateChild 源码分析”请见下文优化二。


实际应用淘特为例。直播 Tab 的视频预览功能为例,最初直播 Tab 的视频播放 index 通过状态层层传递给子组件,一旦状态变更,顶层 setState 触发播放 index 更新, 造成整个页面刷新。但实际整个页面需要更新状态的只有“需要暂停的原 VideoWidget”和“待播放的 VideoWidget”, 我们改为监听机制,页面中的所有 VideoWidget 注册监听,顶层用 EventBus 统一分发播放 index 至各 VideoWidget,局部 Widget Check 后改变自身状态。


再比如详情页,由于使用了“上一个页面借图”的功能,监听到滚动后隐藏借的图,但 setState 的调用节点放在了详情顶层 Widget,造成了全局刷新。实际该监听刷新逻辑可下放至“借图组件”,降低“脏树”的大小。



缓存不变的 Widget

缓存不变的 Widget 有 2 大好处。1.被缓存的 Widget 将无需重复创建, 虽然 Flutter 官方认为 Widget 是一种非常轻量级的对象,在实际业务中,Build 耗时过高仍是一种常见现象。2.返回相同引用的 Widget 将使 Flutter 停止该子树后续遍历, 即 Flutter 认为该子树无变化无需更新。原理请看下图“Element.updateChild 源码分析”



应用场景以淘特实际页面为例。详情页部分组件使用了 DXWidget,理论上组件内容一经创建后当次页面生命周期不会再有变化,此种情况即可缓存不变的 Widget,避免重复动态渲染 DX,停止子树遍历。


Feed 流的 Item 组件,布局复杂,创建成本较高,理论上创建一次后内容也不会再变化,但 item 可能被删除,此时应该用 Objectkey 唯一标识组件,防止状态错位。




减少不必要的 build(setState)

直播 Tab 用到一个埋点曝光组件,经过 DevTools 检查,发现其在每一次进度回调中重新创建 itemWidget,虽然这不会造成业务异常,但理论上 itemWidget 只需被创建一次,这块经排查是使用组件时误传了 builder 函数,而不是直接传 itemWidget 实例。


详情页的逻辑非常复杂,AppBar 根据滚动距离实时计算透明度,这会导致高频的 setState,实际上透明度变化前后应该满足一个差值后才应刷新一次状态, 为了性能考量,透明度应该只有少数几种值变更。



多变图层与不变图层分离

在日常开发中,会经常遇到页面中大部分元素不变,某个元素实时变化。如 Gif,动画。这时我们就需要 RepaintBoundary,不过独立图层合成也是有消耗,这块需实测把握。以淘特为例。


直播 Feed 中的 Gif 图是不断高频跳动,这会导致页面同一图层重新 Paint。此时可以用 RepaintBoundary 包裹该多变的 Gif 组件,让其处在单独的图层,待最终再一块图层合成上屏。


同理, 秒杀倒计时也是电商常见场景, 该组件也适用于 RepaintBoundary 场景。



避免频繁的 triggerGC

因为 AliFlutter 的关系,我们得以主动触发 DartGC,但 GC 同样也是有消耗的,高频的 GC 更是如此。淘特之前因为 iOS 的内存压力,在列表滚动停止时 ScrollEndNotification 则会触发 GC,ScrollEndNotification 在每一次手 Down->up 事件后都会触发一次,如果用户多次触摸,则会较为频繁的触发 GC,实测影响 Y67 4 帧左右的性能,这块增加页面不可见时 GC 和在 Y67 等 android 低端机关闭滑动 GC,提高滑动性能。


大 JSON 解析子线程化

Flutter 的 isolate 默认是单线程模型,而所有的 UI 操作又都是在 UI 线程进行的,想应用多线程的并发优势需新开 isolate 或 compute。无论如何 await,scheduleTask 都只是延后任务的调用时机,仍然会占用“UI 线程”, 所以在大 Json 解析或大量的 channel 调用时,一定要观测对 UI 线程的消耗情况。在淘特中,我们在低端机开启 json 解析 compute 化,不阻塞 UI 线程。


尽量减少或降级 Clip、Opacity 等组件的使用

Flutter 中,Clip 主要用于裁剪,裁矩形、圆角矩形、圆形。一旦调用,后续所有的绘图指令都会受其 Clip 影响。有些 ClipRRect 可以用 ShapeDecoration 代替,Opacitiy 改用 AnimatedOpacity, 针对图片的 Clip 裁切,可以走定制图片库 Transform 实现。


降级 CustomScrollView 预渲染区域为合理值

默认情况下,CustomScrollView 除了渲染屏幕内的内容,还会渲染上下各 250 区域的组件内容,即如双列瀑布流,当前屏幕可显示 4 个组件,实际仍有上下共 4 个组件在显示状态,如果 setState(加载更多时),则会进行 8 个组件重绘。实际用户只看到 4 个,其实应该也只需渲染 4 个, 且上下滑动也会触发屏幕外的 Widget 创建销毁,造成滚动卡顿。高性能的手机可预渲染,淘特在低端机降级该区域距离为 0 或较小值。


高频埋点 Channel 批量化操作

在组件曝光时上报埋点是很常见的行为,但在快速滚动的场景下, 瞬间 10+ item 的略过,20+ channel 的调用同样会占用一定的 UI 线程资源和 Native UI 线程资源。这里淘特针对部分场景做了批量、定时上传, 维护一个埋点队列,默认定时 3S 或 50 条,业务不可见时上报,合并 20+channel 调用为单次。业务也可在合适时机点强制 flush 队列上报, 同时在 Native 侧,将埋点行为切换至子线程进行。

其他有效优化措施

部分业务特效,业务繁忙度在低端机上都是可以适度降级的,如淘特将 Feed 视频预览播放延迟时间从 500ms 降为 1.5S,Feed 流预加载阈值距离从 2000+降为 500,图片圆角降直角等降级措施的核心思路都是先保证最低端的用户也能用的顺畅,再美化细节锦上添花。


Flutter 在无障碍开启情况下,快速滚动场景存在性能问题,如确定业务无需无障碍或用户误触发无障碍,可添加 ExcludeSemantics Widget 屏蔽无障碍。


通过 DevTools 检测,发现 high_available 高可用帧率检测在老版本存在性能问题,这块可升级插件版本或低端机屏蔽该检测。

解法 - 优化案例总结

上述十条优化实践,抛开细节看原理,大致分为以下几类, 融会贯通,实践出真知。


如何提高 UI 线程性能:


  • 如何提高 build 性能

  • 降低遍历出发点,降低 setState 的触发节

  • 停止树的遍历,不变的内容,返回同样的组件实例、Flutter 将停止遍历该树(SlideTransition)

  • 减少非必要的 build(setState)

  • 如何提高 layout 性能

  • layout 暂时不太容易出问题

  • 如何提高 paint 性能

  • RepaintBoundary 分离多变和不变的图层,如 Gif、动画, 但多图层的合成也是有开销的

  • 其他

  • 耗时方法如大 JSON 解析用 compute 子线程化

  • 减少不必要的 channel 调用或批量合并

  • 减少动画

  • 减少 Release 时的 log

  • 提高 UI 线程在 Android/iOS 的优先级

  • 列表组件支持局部 build

  • 较小的 cacheExtent 值,减少渲染范围


如何提高 GPU 线程性能:


  1. 谨慎 saveLayer

  2. 尽量少 ClipPath、一旦调用,后续所有绘图指令需与 Path 做相交。(ClipRect、ClipRRect 等)

  3. 减少毛玻璃 BackdropFilter、阴影 boxShadow

  4. 减少 Opacity 使用,必要时用 AnimatedOpacity

解法 - 测量工具


工欲善其事,必先利其器。工具主要分为以下两块。


  1. 流畅度检测:无需侵入代码的流畅度检测方案有几种, 既可以通过 adb 取 surfaceflinger 数据, 也可以基于 VirtualDisplay 做图像对比,或者使用官方 DevTools。第三方比较成熟的如 PerfDog

  2. 卡顿排查:DevTools 是官方的开发配套工具,非常实用

  3. Performance 检测单帧 CPU 耗时(build、layout、paint)、GPU 耗时、Widget Build 次数

  4. CPUProfiler 检测方法耗时

  5. Flutter Inspector 观察不合理布局

  6. Memory 监控 Dart 内存情况

DevTools

Flutter 分为三种编译模式,Debug/Release 大家都很熟悉,Debug 最大特性为 HotReload 可调试,Release 为最高性能,Profile 模式则取其中间,专用于性能分析,其产物以 AOT 模式无限接近 Release 性能运行,又保留了丰富的性能分析途径。


如何以 Profile 模式运行 flutter?


如果是混合工程,android 为例,在 app/build.gradle 添加 profile{init with debug}即可, 部分应用资源区分 debug/profile,也可 Copy 一份 profile。当然,更 hack 更彻底的方式,可直接修改 $flutterRoot/packages/flutter_tools/gradle/flutter.gradle 文件中 buildModeFor 方法,默认返回想要的 Profile/Release 模式。


如何在 Profile 模式下打开 DevTools?


推荐使用 IDE 的 flutter attach 或者 命令行采用 flutter pub global run devtools,填入 observatory 的地址,即可开始使用 DevTools。


Flutter Performance&Inspector


以 AS 为例,右侧会出现 Flutter Performance 和 Inspector2 个功能区。Performance 功能区如下图:



Overlay 效果如下图。可以看到有 2 排柱状图,上方为 GPU 帧耗时,下方为 CPU 耗时,实时显示最近 300 帧情况,当当前帧耗时超过 16ms 时,绿色扫描线会变红色, 此图常用于观察动态过程中的“瞬时卡顿点”。



Inspector 较为简单,可观看 Widget 树结构和实际的 Render Tree 结构,包含基本的布局信息,DevTools 中 Inspector 包含更详细信息。




DevTools&Flutter Inspector



DevTools&Performance




Performance 功能是性能优化的核心工具,这里可以分析出大部分 UI 线程、GPU 线程卡顿的原因。为方便分析,此图用 Debug 模式得来,实际性能分析以 Profile 模式为准。


如上图 1 所示,Build 函数耗时明显过长,且连续数十帧如此,必然是 Build 的逻辑有严重问题。理论上 Widget 创建一次后状态未改变时无需重建。由前文淘特案例可以发现,这里实际是业务错误的在滚动进度回调中重复创建 Widget 所致。实际的 Build 应只在瀑布流 Layout 逻辑中创建执行 2 次。


Paint 函数详情可在 debug 模式通过 debugProfilePaintsEnabled=true 开启。当多变的元素与不变的元素混在同一图层时可造成图层整体的过度重复绘制, 如元素内容无变化,Paint 函数中也不应出现多余元素的绘制耗时。通过前面提及的 Repain RainBow 开关或 debugRepaintRainbowEnabled=true, 可实时观察重绘情况,如下图所示。


每一个图层都有对应的不同颜色框体。只有发生 Repaint 的图层颜色会发生变化,多余的图层变色,我们就要排查是否正常。



GPU 耗时过多一般源于重量级组件的过度使用如 Clip、Opacity、阴影, 这块发现耗时过多可参考前文解法进行优化或降级, 关于 GPU 更多的优化可参考 liyuqian 的高性能图形引擎分享。


在图 1 最下方的 CPU Profile 即代表当帧的 CPU 耗时情况,BottomUp 方便查找最耗时的方法。


DevTools&CPU Profiler



在 Performance 的隔壁是 CPU Profiler,这里用于统计一段时间内 CPU 的耗时情况,一般根据方法名结合经验判断是业务异常还是正常耗时,根据 visitChilddren-->getScrollRenderObject 方法名搜索,发现高可用帧率监控存在性能问题。


Devtools 还有内存、Debugger、网络、日志等功能模块,这块流畅度优化中使用不多,后续有更好的经验再和大家分享。

DebugFlags&Build


上图是一张针对 build 阶段常见的 debug 功能表, debugPrintRebuildDirtyWidgets 开关将在控制台打印什么树当前正在被重建,debugProfileBuildsEnabled 作用同 Performance 的 Track Widget Builds,监控 Build 函数详情。前 3 个字段在 debug 模式使用,最后一个可在 Profile 模式使用。

DebugFlag&Paint


上图是一张针对 Paint 阶段常见的 debug 功能表。debugDumpLayerTree()函数可用于打印 layer 树,debugPaintLayerBordersEnabled 可在每一个图层周围形成边界(框),debugRepaintRainbowEnabled 作用同 Inspector 中的 RainBow Enable, 图层重绘时边框颜色将变化。debugProfilePaintsEnabled 前文已提到,方便分析 paint 函数详情。


展望

以上便是淘特 Flutter 流畅度优化第一期实践,也是体感优化最明显的的一期优化。但距离极致的用户体验目标仍有不小的差距。集团同学提供了很多秀实践学习。如 UC Hummer 的 Engine 流畅度优化, 闲鱼的局部刷新复用列表组件 PowerScrollView、线上线下的高精准多维度检测卡顿,及如何防止流畅度优化不恶化的方案, 淘特也在不断学习成长挑战极限,在二期实践中,为了最极致的体验,淘特将结合 Hummer 引擎,深度优化高性能图片库、高性能流式容器、建立全面的线下线上数据监控体系,做一个”让用户爽的淘特 App“。

参考资料

  • Flutter 性能测试与理论:https://www.bilibili.com/video/BV1F4411D7rP

  • Flutter Europe 演讲:https://www.youtube.com/watch?v=vVg9It7cOfY

  • 复杂业务如何保证 Flutter 的高性能高流畅度:https://zhuanlan.zhihu.com/p/134024247

  • Flutter 官方 DartTools 讲解:https://www.youtube.com/watch?v=nq43mP7hjAE

  • 深入理解 Flutter 的图形渲染:https://www.bilibili.com/video/BV1ab411T7nM

  • Flutter 官方源码:https://github.com/flutter/flutter


关注【阿里巴巴移动技术】微信公众号,每周 3 篇移动技术实践 &干货给你思考!

发布于: 刚刚
用户头像

还未添加个人签名 2018.07.07 加入

阿里巴巴移动&终端技术官方账号。

评论

发布
暂无评论
淘特 Flutter 流畅度优化实践