简介:Flutter 有很多长处,特地是对于开发者来说,跨平台多端反对,丰盛的 UI 组件库和交互成果,申明式 UI,React 的更新形式,Hot-reload 进步开发效率等等。尽管它在渲染性能上有不少缺点,然而某种程度上,某些缺点也是为了实现更高层次的设计指标而不得不接受的后果。
作者 | 萧逸
起源 | 阿里技术公众号
我在《Flutter vs Chromium 动画渲染的比照剖析》一文中对 Flutter 和 Web (Chromium) 的各种动画的实践性能优劣进行了剖析,其中一个次要论断是,因为惯性滚动解决机制和光栅化机制的不同,Web (Chromium) 的惯性滚动动画性能实践上要远远优于 Flutter。而在一些曾经上线的应用 Flutter 的业务中,业务方也继续给咱们反馈了这些业务在中低端 Android 手机上存在比较严重的惯性滚动性能问题:
业务 A 的页面较为简单,然而在低端手机上均匀帧率在 40 ~ 50 之间,中端手机在 50 ~ 55 之间,低端机存在较为显著的卡顿问题。
业务 B 的页面比较复杂,业务逻辑也较为简单,在低端手机上均匀帧率更是低到最低 30 多帧(35 ~ 45 之间),中端手机也是在 50 左右,并且存在较为频繁的长时间卡顿,低端机存在比较严重的卡顿问题,中端机也不太晦涩。
而以咱们长期的教训数据,对于 Web 来说,即便在低端手机上,较为简单的页面惯性滚动帧率个别也在 50 以上,也较少长时间的卡顿,达到根本晦涩的程度。并且刚好业务 B 有齐全一样的 Native 版本,它比照 Flutter 版本,帧率广泛高了 5 ~ 10 帧左右。
所以尽管咱们没有找到同一个页面的三个不同版本进行严格的比对,然而基于上述的测试数据和咱们长期的教训,很容易得出结论是,在惯性滚动的性能上:
Web (Chromium) > Native (Android) > Flutter (Android)
咱们在不同设施上对上述业务页面在惯性滚动过程中进行 trace 的抓取,联合 Flutter 的代码对 trace 文件进行剖析,理解 Flutter 渲染流水线在惯性滚动过程中各个环节的调度,理解各个环节的可能耗时和哪些环节可能成为性能瓶颈。在剖析的过程中,咱们对 Flutter 的渲染机制有了更深刻的理解,这篇文章就是比照 Web (Chromium) 和 Native (Android),对 Flutter 的渲染性能问题进行深入分析,特地是剖析惯性滚动性能蹩脚的起因。
阐明:
这里的帧率数据给的是一个范畴是因为咱们应用了几种不同的滚动速度进行测试,一般来说滚动速度越快,均匀帧率就越低。
iPhone 根本不存在所谓的低端机,iOS 整体体现都还能够,不同实现的差别不大,所以咱们目前次要的测试和优化都是在 Android 上进行。
一 写在后面的论断
Flutter 有很多长处,特地是对于开发者来说,跨平台多端反对,丰盛的 UI 组件库和交互成果,申明式 UI,React 的更新形式,Hot-reload 进步开发效率等等。尽管它在渲染性能上有不少缺点,然而某种程度上,某些缺点也是为了实现更高层次的设计指标而不得不接受的后果。
比方 Dart 语言原生对异步编程有良好的反对,利用开发者不须要去编写简单和容易出问题的多线程代码,就能够无效地防止主线程长时间阻塞,编写出性能良好的 UI。然而在惯性滚动这样对性能要求十分高场景下,可能几毫秒的阻塞都会导致掉帧,短少真正的多线程编程能力某种程度就变成了一种妨碍(Android 上你甚至能够在其它线程对 View 做非 UI 间接相干的操作)。
又比方应用 Immutable Widget 作为 UI Configuration 的设计是申明式 UI 和 Hot-reload 的根底,但还是会引入额定的开销和丢失足够的灵活性,利用无奈间接管制 UI 组件的生命周期,无奈间接管制 UI 组件的布局和绘制,这同样障碍了惯性滚动的性能优化。
咱们是 UC 浏览器内核团队,次要负责 Chromium 和 Flutter 定制引擎的开发,咱们的 Flutter 定制引擎以 Hummer 为代号。而对咱们内核团队来说,要做的就是在了解 Flutter 这些缺点的同时,去钻研是否存在无效地进行部分改良,或者从其它设计层面上对某些缺点进行躲避的办法,让利用开发者既能够充分利用 Flutter 的劣势,又不必过于放心它存在的问题。
总的来说下半年的工作目前看来还是获得了不错的成绩,也根本实现了让 Flutter 惯性滚动性能对标原生的指标,下图对业务 B 页面的测试数据比拟直观地展现了咱们优化的后果。
这里电影帧是指 1000 / 24 约 40 毫秒,2 个电影帧 / min 是指间断滚动一分钟内呈现超过 80 毫秒卡顿的次数。
二 Web (Chromium) vs Flutter
Web (Chromium) 在惯性滚动上是有非常明显的机制劣势的,这跟 Web 渲染引擎为了适应 Web 页面的高复杂度,高不确定性无关,甚至某种程度上就义了一些渲染成果和其它动画的渲染性能。Web (Chromium) 在惯性滚动上的劣势次要体现在以上两方面:
Chromium 有残缺独立的合成器驱动惯性滚动动画的运行,有独立的合成线程,惯性滚动动画的更新和主线程更新 DOM 树是不同步的,主线程运行 JS,Build & Layout 不会阻塞合成线程。
Chromium 的分块异步光栅化机制一方面缩小了惯性滚动动画过程中图层的反复光栅化,另一方面光栅化不会阻塞合成线程的合成输入。
比照 Web (Chromium),Flutter 在上述两方面都存在比拟显著的劣势:
Flutter 须要依赖于 Relayout 来驱动惯性滚动动画,滚动容器内的元素在滚动过程中每一帧都须要 Relayout,不过这个个别耗时不高。Flutter 的有限长列表个别都采纳 Lazy Build 的形式生成列表单元,当列表单元靠近可见区域的时候,框架才调用利用提供的 Builder 生成列表单元的 Widget 树并进行布局,新挂载的列表单元的 Build & Layout 通常耗时较长,在上述业务页面中,可能消耗 10 毫秒以上,甚至几十毫秒,特地是单帧内须要 Build 多个单元的状况,它们是导致掉帧的次要起因。从上图 trace 中咱们很容易发现,失常速度滚动下,在 Flutter UI 线程 Frame 的阶段,大部分状况下耗时不高,然而每几帧就会呈现一次耗时较长的 Frame,从上图看耗时较长的 Frame 曾经靠近甚至超过一个 vsync 周期,滚动速度越快,呈现耗时较长的 Frame 的频率就越高,耗时也可能越长,它的耗时次要就来自新挂载列表单元的 Build & Layout。
Flutter 采纳的以间接光栅化为主,间接光栅化为辅的同步光栅化机制,在合成输入过程中进行光栅化,光栅化的耗时会间接影响动画的性能。以理论业务为例子:
业务 A 的页面较为简单,光栅化耗时大部分在 3 ~ 5 毫秒之间,除了偶然稳定较高外,根本没有造成阻塞,所以业务 A 的大部分掉帧都是 Flutter UI 线程的 Frame 耗时较高导致;
业务 B 的页面比较复杂,光栅化耗时大部分在 7 ~ 10 毫秒之间,偶然稳定超过 10 毫秒,所以局部掉帧次要是光栅化导致的;
实际上咱们还碰到一个页面因为大范畴应用 Backdrop Filter 导致光栅化耗时十分高,在低端机上只有 10 ~ 20 帧,不过这个能够在利用层面做一些优化来防止;
总的来说,Flutter 在惯性滚动过程的掉帧大部分都来自 Flutter UI 线程的阻塞,新挂载列表单元的 Build & Layout 耗时过长是次要起因。然而对于一些比较复杂的页面,光栅化耗时较长也是一个导致掉帧的起因。
咱们在 Chromium 光栅化革新 – 混合光栅化 比照了不同光栅化机制在合成输入过程中的光栅化 + 合成输入的耗时,异步光栅化机制在这方面会有显著的劣势,这也是咱们在 U4 4.0 上采纳了混合光栅化的起因。
Flutter 尽管提供了 KeepLive 机制用于防止列表单元滚出可见区域被回收,从新滚入可见区域又从新 Rebuild & Relayout,然而 KeepLive 机制并不适用于第一次显
示的列表单元,并且在有限长列表场景很容易造成内存爆炸,实用场景不多。
三 Native (Android) vs Flutter
如果说 Web (Chromium) 因为机制的起因,惯性滚动性能显著优于 Flutter,这个比拟容易了解。那么 Native (Android) 在机制上其实跟 Flutter 是比拟相似的,为什么它的性能也会优于 Flutter 呢?
Android 有限长列表个别应用 RecyclerView 实现,而 RecyclerView 反对子 View 树级别的复用,使得新挂载的列表单元在 RecyclerView 的反对下,只须要更新复用的子 View 树的数据而后部分重排即可,耗时会大大少于 Flutter 整个列表单元的残缺 Build & Layout,这是 Native (Android) 的有限长列表滚动更晦涩的次要起因。不过除此以外,还有很多因素也会影响到 Flutter 的晦涩度。
跟 Native 相比拟,Flutter UI 线程会显得更拥挤。Dart Isolate 的内存堆是隔离的,这点比拟像 JavaScript,Isolate 之间的关系更像是多过程而不是多线程,导致了一些多线程优化很难实现。利用通常要注册多个回调来解决内部传入的数据或者事件,这些回调接管内部数据或者事件,进行解决后更新外部数据(Model),通常这些回调都须要在 UI 线程执行。如果它们集中频繁地产生,即便单次耗时不高,也很容易造成 Flutter UI 线程的阻塞,简略说就是这些非 UI 工作的频繁执行可能会导致惯性滚动过程中 UI 工作的提早,最终导致掉帧,然而 Dart Isolate 的限度,对外部数据的更新又必须在 UI 线程上进行。
大部分利用都是部分应用 Flutter 开发,须要跟 Native 进行混用,这就导致了利用很难应用 SurfaceView,而须要应用 TextureView。TextureView 会带来一些额定的性能问题,除了更高的 GPU 开销外,TextureView 的绘制机制也容易呈现因为调度的不合理而导致掉帧。
最初尽管 Android 和 Flutter 都是以间接光栅化为主,间接光栅化为辅的同步光栅化机制。然而将 Skia 作为 UI 的光栅化引擎,比起为 UI 专门定制的光栅化引擎可能还是存在一些缺点:
Skia GPU 光栅化的过程,波及将通用的 2D 绘制指令转换成一种靠近 GPU 指令的外部模式,而后通过进一步优化后输入最终的 GPU 指令,为 UI 专门定制的光栅化引擎实践上能够缓存第一步的后果,缩小每一帧光栅化的耗时;
Skia 作为一个通用的光栅化引擎,外部实现是线程无感的,而为 UI 专门定制的光栅化引擎能够更容易应用多线程来将光栅化过程中局部 CPU 工作并行化,比方生成字型或者门路顶点等工作;
不过咱们没有理论去比拟两者的光栅化性能差别,这里只是一些实践剖析。
四 利用层面优化和局限性
针对 Flutter 的惯性滚动性能问题,不少利用也尝试了各种优化计划,比方闲鱼的计划就比拟有代表性。针对新挂载列表单元的 Build & Layout 耗时过长,闲鱼的优化计划是 Element 复用和分帧渲染。
Element 复用其实就是参考 RecyclerView 的子 View 树复用,实践上能够防止从新创立列表单元的 Element 树和 RenderObject 树的工夫开销。然而比照 Native,依然须要从新构建 Widget 树,并把新的 Widget 树跟旧的 Element 树进行绑定,并通过 Element 树去更新 RenderObject 树。而 Native 则能够间接复用 View 树,而后更新若干子 View 的数据即可,这部分的开销依然比优化过后的 Flutter 要低。
分帧渲染的思路是每个列表单元提供两个版本的 Widget 树,除了完整版,还有一个简化版作为占位符。如果单帧内曾经 Build 过一个残缺版本的单元,在须要 Build 第二个单元时就只 Build 简化的版本,这样能够防止单帧内多个列表单元的 Build & Layout 叠加在一起造成更大的阻塞。它的局限性是次要实用于列表单元较小,惯性滚动速度较快,一帧滚动会呈现多个列表单元须要 Build & Layout 的场景,对防止更长时间的卡顿有肯定作用。只是这个优化 Android Native 看起来也齐全能做,并且因为 Android 利用能够间接管制 View 是否参加布局和绘制,实践上做起来也更简略,成果也更好。
总的来说,Flutter 利用的一些优化,要不是 Native 原本就曾经实现,并且成果更好;就是 Native 同样也能够实现,而且实现起来更简略,成果也更好,并且其它一些影响 Flutter 性能的因素在利用层面无奈进行优化。
所以 Flutter 利用优化起来可能比 Native 更麻烦,最初的成果也还是比不上 Native。一个优化后的 Flutter 利用,比起一个优化后的 Native 利用,在惯性滚动上还是会有肯定性能差距。
五 咱们的优化尝试
作为一个引擎团队,咱们冀望实现的指标是从框架和引擎层面对 Flutter 渲染流水线的方方面面进行优化,使利用在不须要改变或者极少量改变就能实现根本对标原生的惯性滚动晦涩度,如果利用自身再进一步优化,甚至有可能取得优于原生的成果。
咱们尝试了各式各样的优化,包含:
优化线程的优先级设置,更好地保障渲染流水线的前台线程,UI 和 Raster 线程不会因为无奈获取到 CPU 调度而阻塞;
优化渲染流水线的 vsync 调度,缩小一些不必要的耗时和空等;
优化渲染流水线针对 TextureView 绘制的调度,躲避 TextureView 绘制机制的副作用;
重构渲染流水线的调度逻辑,通过更深的流水线深度来减少输入的吞吐量,使得输入更安稳间断;
优化一些布局算法,缩小布局耗时;
优化新挂载列表单元的 Build & Layout 的调度,缩小其成为性能瓶颈的可能,比如说将新挂载单元的 Build 和 Layout 拆分到不同帧去执行;
优化光栅化性能,比方更好地反对客户端应用相似 Web 开发的 Opacity Hack 的技巧,通过应用间接光栅化来缩小光栅化耗时。
从目前来看,局部优化尝试的成果还是非常显著,有些优化的覆盖面很广,实用于简直所有的场景,而有些优化对特定场景成果比拟好。总的来说,测试的业务页面运行在咱们优化过后的引擎,整体晦涩度可能显著晋升一个台阶,也根本实现了咱们对标原生晦涩度的指标。在后续的文章中,我会逐渐介绍咱们所做的一些优化,同时咱们也会争取将一些优化的代码提交回社区。
原文链接
本文为阿里云原创内容,未经容许不得转载。