作者:谢伟(韦圣)
不同的业务背景引出不同的技术诉求,“用户体验特爽”是淘特的不懈谋求,本文将介绍笔者退出淘特以来在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 篇挪动技术实际&干货给你思考!