当前位置: 首页 > 科技观察

Flutter研发模式重度使用下的页面性能优化实践

时间:2023-03-15 20:16:10 科技观察

一、Flutter页面性能优化的挑战淘宝特价版是集团内多场景使用Flutter技术的应用,拥有超过1亿用户。目前我们的首页、详情、店铺、我的、看看短视频、评价、设置等二级页面都是用Flutter技术搭建的。我们发现使用Flutter经常会遇到性能问题。因为Flutter只是一个严格意义上的“UI渲染框架”,它通过异步实现分线程渲染UI,通过Skia保证两端的“渲染一致性”。但是,子线程执行和渲染,动态库打包的这些策略并不是“一刀切”的,会导致页面打开性能的损失和交互时间的增加。试想一下,app启动时动态库加载的动态绑定(影响启动时间),页面启动时主线程启动页面,但ui渲染需要等待Flutter的子线程执行完毕,render,在低端机器上页面会暂时空白(页面Unrendered影响交互时间,虽然fps是骗人的增加了)。Flutter存在性能瓶颈,但是我们在大量使用Flutter的情况下如何实现性能优化呢?本文将在基础链接上分享我们对各个Flutter页面优化策略的实践!2、模块级混合——首页优化实践一开始,首页都是使用Flutter+DXFlutter(Flutter的一个UI动态框架)实现的,所有业务实现都ok。但是在版本发布的时候,测试同学发现首页的启动性能比之前的版本突然下降了1秒。这个问题是不可避免的,因为Flutter是一个动态库,需要延迟加载和绑定,同时DXFlutter中大量的模板逻辑会极大地消耗性能。这个问题很难解决。当时我们对于是否继续所有Flutter产生了分歧,但是优化引擎和DXFlutter还是退回到了Native实现。如果回归Native,首页和搜索分类标签的技术方案都要切换回Native,代价很大。最终,基于专版的现状和经验,我们决定在应用启动时的首页推荐使用Native实现,但搜索实现的其他tab分类会继续使用Flutter。这不会影响搜索业务的研发模式,避免Flutter。造成启动性能的损失。但是,这个方案会遇到一个技术挑战:我们使用的Flutter混合栈FlutterBoost只支持页面级混合,不支持页面内模块级混合。由于Flutter是单窗口设计策略,如果模块层级混合,必然会遇到Flutter页面生命周期的管理和渲染窗口大小的一致性问题。之前的问题是因为Flutter是单引擎造成的。模块切换时,新模块显示需要连接引擎重新触发渲染,否则页面会空白或无法交互。这已经在FlutterBoost中完成了。后面的问题是单窗口,Flutter页面弹出Native页面会导致页面布局问题。但是,模块级混合在技术上显然是可行的。在AliFlutter正武和来易外的参与下,我们很快开发了一个模块级的混合容器,可以容纳Native、Flutter,甚至WebView等其他类型。如下图:我们使用一个FlutterWrapperVC来解决基于FlutterBoost的单引擎渲染问题,根据模块可见性在FlutterEngine和虚拟机之间切换,保证当前可见的模块能够正常渲染,执行底层颤振代码;然后通过强制修正窗口大小解决问题单窗口问题,解决布局问题。最后,我们也考虑了模块的复用性,将这个能力组件化,封装在这里:LTaoUIKit。podLTaoUIKit'0.0.3.89'基于此解决方案后,经过此修改,首页的启动实践至少提高了1秒。更重要的是,首页的研发就像在围棋中打造两只眼睛,过着一种生活。基于原生dxcontainer实现推荐页面后,可以直接复用淘宝等成熟APP的优化经验。稍后我们将优化RT、基础DX模板、本地化图标和压缩图像。首页启动性能稳步提升。同事后来一步步管理首页的启动链接,做了比较系统的治理建设:3.数据预取和FFI——纯Flutter页面的优化实践以上讨论了模块混合Flutter页面的性能优化.小节说整个页面是Flutter实现的一种优化策略,应该是大多数Flutter开发者通用的。这样,以特价页面为例,我们将之前的优化结果优化了100多ms。以下是具体数据:Android(vivoy67):80~100msiOS(iPhone6):120~200ms首先,Flutter页面会遇到哪些性能瓶颈?从Flutter机制来看,它其实是一个可以更好解决“多端一致性问题”的“UI渲染框架”。虽然它提供了通过bridge访问native,但是Flutterbridge的性能极差。涉及到线程切换、字符编码等问题(后面会讲到)。所以,我们在使用Flutter的时候,应该避免直接通过Flutter来解决IO等资源访问任务,这些任务尽量放在native端。显然,应用的一个页面的启动,往往需要请求服务端准备渲染数据。同时,从路由跳转到通过Engine初始化NativeVC或activity,再到通过Engine构建Rasterrizer,也涉及到Engine层面的线程等待。事实上,这段时间可以做很多事情。我们可以把服务器端的数据请求放到这个阶段,也就是我们要做的“数据预取”。其实“数据预取”在淘宝上已经大规模使用,但是在Flutter页面上显示的优越性会更强,因为Flutter页面多线程切换太多,容易陷入通道桥的陷阱。比如我们的详情页一开始也是使用了数据预取,但是数据预取的调用是从Flutter发起的,性能好像有提升,但是没有那么明显。为什么?下面是一个Flutter请求mtop的流程图,从中可以窥见一斑:上图中的UI线程指的是Flutter的UI线程,不是系统主线程。请求从“开始”开始,绕来绕去。需要经过2次线程切换等待,多次数据编解码,性能损失比较大。分析如下:首先,一旦设备CPU在某种情况下紧张,那么Flutter的请求/数据返回将很长时间都不会投递到native或Flutter。其次,当数据量很大时,数据的编码和解码也会花费更多的时间。最后,页面打开经常会遇到Engine和Native谁先启动的问题。比如VC启动的时候,这个时候是不可能马上给Flutter发消息的,因为FlutterEngine可能还没准备好,这个时候消息丢了,双方都不知道。这个问题在FlutterBoost中遇到的比较多。我们最终通过以下策略解决:在Native端发起数据预取,页面路由构造NativeVC/Activity时立即发起mtop等请求。mtop返回的数据并没有通过channelbridge返回Flutter层,而是直接通过ffi机制被Flutter读取。上面native端必须临时存储数据,但是为了避免长期引用造成资源泄漏,采用了LRU策略来缓存数据(iOS不能使用NSCache,猜猜为什么)。考虑到一些页面数据可以持久存储以备下次使用,我们构建了一个多级缓存策略。以上能力全部组件化,供其他业务复用。详细设计如下:首先,基于ffi和native端数据预取,优化后的数据请求链接如下:右上角之所以有通道桥,是为了解决数据刷新的情况原生请求返回比Flutter页面渲染慢。其次,我们构建了多级缓存策略和缓存失效复用策略,支持对部分页面数据的持久化复用,以提升首屏渲染性能:以缓存复用策略为例,我们支持如下strategies:Aggressivetype:No.第二次请求直接使用上次缓存的数据,而不是立即刷新数据,缓存自然过期后再刷新。这种策略适用于页面数据不经常变化的情况。普通型:第二次请求可以使用之前的缓存,但仍然需要立即请求并刷新数据。这种策略比较通用,适用于数据变化不频繁的情况。保守型:第二次请求不能使用上一次的缓存,需要请求最新的数据。适用于实时性强的页面数据渲染。最后,我们封装这些能力。比如在iOS端,我们集成了一个单独的SDK:podLTPrefetch'1.0.1.19'。目前,详情、我的、店铺、迷你详情等基础环节均通过该方案进行了优化。启动实践都得到了改进。4.其他优化实践还有很多其他优化实践。有的在淘宝实践过,有的根据Flutter的特点修改。这里就不细说了,只列出我们用过的:详情借用主页。资源压缩及本地预设:如首页dx模板预设、Json压缩及预设、图片压缩及预设。数据提前异步加载。比如我的页面数据其实是在用户登录的时候异步加载和缓存的,然后通过上面的缓存更新策略进行更新。优化服务端RT,简化协议。其他。5、最后我们的优化策略更多的是在上层应用上进行优化。UC在FlutterEngine层面做了优化。后期可以考虑使用他们的引擎。相信页面打开性能会更上一层楼。同时,上面的ffi和数据预取也可以做的更积极一些。比如通过Dart2Native的方式,完全实现了Json数据的encode和decode的本地实现以及容器访问的本地实现,估计时间至少可以提升50ms。