共计 7236 个字符,预计需要花费 19 分钟才能阅读完成。
作者:谢伟(韦圣)
不同的业务背景引出不同的技术诉求,“用户体验特爽”是淘特的不懈谋求,本文将介绍笔者退出淘特以来在 Flutter 晦涩度方面的诸多优化实际,这些优化不波及 Engine 革新、不波及高大上的“轮子建设“,只需仔细粗疏深刻业务抽丝剥茧,保持理论体感导向,即能为用户体验带来显著晋升,值得 Flutter 开发者将其利用在产品的每一个像素。
背景
淘特具备显明的三大特色:
- 业务特色:淘特领有业界最简单的淘系电商链路
- 用户特色:淘特用户中有大量的中老年用户,大量的用户手机零碎版本较低,大量的用户应用中低端机
- 技术特色:淘特大规模采纳 Flutter 跨平台渲染技术
综上所述:
最简单业务链路 + 最低性能用户群体 + 最新的跨平台技术 ==> 外围问题之一:页面晦涩度受到严厉挑战
Flutter 外围链路 | 20S 疾速滚动帧率 | 卡顿率(每秒卡顿率) |
---|---|---|
直播 Tab | 27 | 7.04% |
我的 | 41.3666 | 7.63% |
详情 | 26.7 | 15.58% |
注:相干数据以 vivo Y67,淘特
3.32.999.10 (103) 测得
指标
晦涩度是用户体验的要害一环,大家都不心愿手机用起来像看电影 / 刷 PPT,尤其是当初高刷屏 (90/120hz) 的遍及,更是极大强化了用户对晦涩度的感知,但晦涩度也跟产品复杂度强相干,也是一次繁与简的取舍,淘特晦涩度一期优化指标:
Flutter 外围链路页面达到高晦涩度(均匀帧率:低端机 45FPS、中端机 50FPS、高端 50FPS)
一期优化后的状态
事项 | 均匀帧率 | 卡顿率 | 晋升成果 |
---|---|---|---|
1. 直播 Tab 举荐、分类栏目 | 46.0 | 0.35% | 帧率进步 19 帧、卡顿率升高 6.7% |
2. 我的页面 | 46.0 | 0% | 帧率进步 4.6 帧,卡顿率升高 7.6% |
3. 详情 | 45.0 | 2% | 帧率进步 18.3 桢,卡顿率升高 13.58% |
旧版 3.32 如视频左,新版 3.37 如视频右。因 uiautomator 工具会触发无障碍性能 ISSUE,此版本比照为人工测试。
视频请见:淘特 Flutter 晦涩度优化实际
除了数据上的显著晋升,体感上,旧版快滑卡顿显著,画面渐变显著,新版则根本打消显著的卡顿,画面间断安稳。
问题
回到技术自身,Flutter 为什么会卡顿、帧率低?总的来说均为以下 2 个起因:
- UI 线程慢了 –> 渲染指令出的慢
- 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 线程性能:
- 审慎 saveLayer
- 尽量少 ClipPath、一旦调用,后续所有绘图指令需与 Path 做相交。(ClipRect、ClipRRect 等)
- 缩小毛玻璃 BackdropFilter、暗影 boxShadow
- 缩小 Opacity 应用,必要时用 AnimatedOpacity
解法 – 测量工具
工欲善其事,必先利其器。工具次要分为以下两块。
- 晦涩度检测:无需侵入代码的晦涩度检测计划有几种,既能够通过 adb 取 surfaceflinger 数据,也能够基于 VirtualDisplay 做图像比照,或者应用官网 DevTools。第三方比拟成熟的如 PerfDog
卡顿排查:DevTools 是官网的开发配套工具,十分实用
- Performance 检测单帧 CPU 耗时(build、layout、paint)、GPU 耗时、Widget Build 次数
- CPUProfiler 检测办法耗时
- Flutter Inspector 察看不合理布局
- 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/vide…
- Flutter Europe 演讲:https://www.youtube.com/watch…
- 简单业务如何保障 Flutter 的高性能高晦涩度:https://zhuanlan.zhihu.com/p/…
- Flutter 官网 DartTools 解说:https://www.youtube.com/watch…
- 深刻了解 Flutter 的图形渲染:https://www.bilibili.com/vide…
- Flutter 官网源码:https://github.com/flutter/fl…
关注【阿里巴巴挪动技术】微信公众号,每周 3 篇挪动技术实际 & 干货给你思考!