原生渲染、自渲染,都是跨端开发领域耳熟能详的词。
但是:
理解这些问题,需要先把渲染系统搞明白。
以原生渲染为列,看看一套渲染系统包括什么,渲染流程是什么。
那么flutter的自渲染是什么?
也就是flutter在原生渲染系统之上,又开辟了一套自己的渲染系统,但最终还要和原生合成。
在web上,flutter采取了一致的思路,在浏览器已经为这个网页初始化好资源后,flutter利用了canvas组件,通过编译为wasm的渲染引擎和Dart代码在上面自绘UI,然后整体合成上屏。
自渲染除了初始化渲染系统慢外,还有一个很大的问题是和原生组件的融合。后面会详细展开。
flutter在不同平台的组件,都是一套dart编写的组件,所以不同平台的组件渲染一致性很高。
react native 使用的是原生渲染,但在排版布局这个环节,改成了用 yoga 这个c库来做 flex排版。这种方式可以让多平台使用一致的flex布局。
但 react native 使用了大量的各平台原生组件,分别由java/kotlin、objective-c/Swift等不同语言编写,这些组件差异,导致 react native 在不同平台的细节无法像flutter那样对齐。
react native 的渲染性能,在跨平台框架中属于偏低的,虽然在 Fabric + JSI 架构下消除了序列化,但它仍然在原生渲染系统之上,又建立了自己的js层虚拟DOM,js代码中发送指令到原生层,在原生层动态创建OS的原生组件。
这是 react native 的渲染流程:
它家的做法又有点不同。
uni-app x 蒸汽模式也基于原生渲染管线,没有自渲染。
在组件系统方面,它和 Compose UI 的Android平台类似,开发了一套自己的组件系统,可以和系统原生组件生态完美融合。但uni在Android、iOS和鸿蒙,是统一的策略,不存在一会儿原生渲染、一会儿自渲染。
uni-app x 蒸汽模式的组件系统采用c++和uts语言开发,不同平台的组件代码一致,良好的保证了跨平台对齐。 并且渲染速度更快。详见benchmark
对比标准的原生渲染,uni-app x 蒸汽模式的变化如下:
了解了渲染系统的基本原理和各家的做法,我们来看看原生渲染和自渲染的性能差异。
前文的渲染系统简述中,已经看出自渲染多做的事情。
初始化渲染资源是一套较重的流程,在原生已经初始化好渲染资源后,自渲染需要再次创建一套自己的渲染系统。但最终也需要由OS统一合成。
可以展开讲下一套自渲染系统要做的事情,在一个应用启动后,原生渲染系统已经准备好,此时发现界面中有flutter的view,
这就是自渲染会比原生渲染在初始化时慢、占用内存和显存更多的原因。
在自渲染系统创建好后,渲染内置组件(如view、text)的速度,取决于渲染系统的代码实现性能。 此时原生渲染和自渲染,谁快谁慢是不一定的,取决于谁的代码写的更好。
不得不说,flutter的渲染内置组件代码写的非常优秀,尤其是在Android和鸿蒙上,部分组件的渲染速度比原生的同功能组件要快。
但注意这里有2个前提:
一旦要绘制平台原生组件(flutter中叫platform View),就不是这回事了。
很多系统组件、三方sdk,用的是原生渲染,这是无法改变的。
当一个界面同时要渲染多套不同管线的内容时,合成问题非常多。
单一渲染体系主要是性能问题;多套渲染体系混合以后,问题就变成了 Composition、Synchronization、Input、Transform 和 Semantics 等系统级问题。
flutter在原生混合渲染上也在持续优化,推出各种不同思路的方案,但官方文档只能列出不同方案的优劣权衡,没有完美方案。本质是多套渲染管线难以调和的问题。
假设有这样的页面:
Flutter Widget
↓
半透明弹层
↓
Native WebView
如果所有内容属于同一个渲染体系,渲染系统知道每一层的位置、透明度和绘制顺序,可以统一处理。
但如果上面是 Flutter 自己绘制的内容,下面是原生view,那么问题就复杂了。
尤其是:
这些效果都依赖正确的前后层关系。
如果来回穿插多层,为了正确合成,要处理很多事情,性能问题就会更加严重。
正常情况下,一个渲染系统内部不同组件层级和透明度,是在渲染系统的渲染线程里统一计算的,动态变更也没问题。
flutter的渲染系统,内部计算自己的widget的层级和半透明关系没问题,但和原生view混合在一起计算层级和半透明,真是难为它了,只能上各种黑科技。
为了处理不同渲染系统的合成,flutter要做很多事情。 所以如果一个界面没有platform view,性能有时能超过原生渲染。 但一旦有带有层级的platform view,性能就不行了。
iOS的液态玻璃效果,被开发者吐槽精准打击 flutter 和 compose-multiplatform iOS版。
这个透明效果与众不同,动态材质、手势变化、光照形变都非常复杂,Apple并未公开其算法。 原生开发可以直接用iOS提供的API,但自渲染很难精准模拟,也达不到系统的省电效果。
更麻烦的是合成,如果液态玻璃下面,一会展现自渲染的组件、一会展现原生或其他渲染方式的platform view,实现起来各种兼容性问题,高性能更不可能。
那开发者不在界面上使用液态玻璃效果是否就能规避此问题?iOS系统有很多隐密的坑,输入框长按的上下文菜单也是液态玻璃的,这个菜单在透显自渲染和自渲染框架下的原生视图都有问题。
注:uni-app x 蒸汽模式,提供了内置的液态玻璃组件,使用系统原生能力,就不会有完全自渲染带来的问题。可以在iOS 26+设备上体验hello uni-app x。录屏如下:
flutter的滚动,是自己模拟实现的,并非系统原生滚动。
在flutter滚动容器中,如果插入一个原生view,比如信息流广告或视频:
Flutter List
├── Flutter Cell
├── Flutter Cell
├── Native Advertisement
├── Flutter Cell
└── Flutter Cell
Flutter滚动容器的滚动位置由 Flutter 管理。
但是中间的原生view,是另一条渲染管线上的,它的排版布局不是flutter的。
前端同学,可以理解下flutter在web上的做法,在canvas上创建了一套自己的渲染管线,滚动也是自己模拟的,此时需要在flutter的列表中插入信息流广告,但这个广告是一个div组件, 如何把这些div,按正确的位置,显示在canvas里的某处,并且随着canvas内部的模拟滚动而移动div的位置?
如果 Flutter 和 原生内容不能在正确的时机提交到同一帧,用户就可能看到位置不同步、抖动或者短暂错位。
保持同步。这不仅是简单地修改一个坐标。还涉及:
为了维持滚动同步,flutter需要多做不少事情来处理各种情况,这造成了性能下降。
并且在高渲染压力下,仍然可能会错帧。
这里并没有否认flutter的努力,它在高版本利用OS的新API做了一定优化,但只是缓解,本质不变。
自渲染和原生渲染,走在不同的渲染管线上。 如果需要一个flutter widget 需要和一个 platform view 做联动动画,甚至多个不同组件需要联动动画,难以流畅衔接。
软键盘动画是很多开发者未预见的坑。因为它不是flutter的 platform view,但软键盘也是属于原生渲染管线的。
软键盘弹起时有一套原生动画,界面上的widget如想跟着动,在不同平台有各自的坑。
基于手势的动画也是难点,手势事件同时传递给自渲染和原生渲染,并且让各自的区域都跟随手势做动画并衔接,性能不佳且复杂动画很容易错帧。
原生和自渲染不仅有两套渲染体系,还存在两套事件体系。
Flutter 有自己的:
Native UI 也有自己的:
flutter和原生,都要各自独立的处理手势事件,点击、缩放、双击、平移、长按等。hit test、冒泡、协商各自独立。
手指在屏幕上滑过多个区域,有的是自渲染、有的是原生渲染,或者多指触摸有的在自渲染区域、有的在原生渲染区域,都会引发事件冲突。
还有手势协商,本来系统原生是有手势协商概念的,可以每个组件吃掉一部分手势余量。 但2套渲染管线,难以高性能协商,需要跨语言、甚至可能跨线程协商,麻烦很多,而且协商的不够快就可能造成渲染异常。
很常见的是嵌套滚动, 比如滚动容器中内嵌了webview组件。在原生渲染中,webview滚动到头后,可以触发原生的父滚动容器的继续滚动。它们直接还可以协商谁吃掉多少滚动余量。
但让flutter的滚动容器和系统webview滚动协商,就难以流畅运行了。
如果 Flutter 对一个区域进行了形变处理:
然后这个区域里面又有原生view,就需要把 Flutter 的 transform 正确传递给原生的合成系统。
否则就可能出现flutter渲染的内容正确形变了,而原生view没变或错位。
反之也一样。
除了platform view,也有一个开发者难以预料的原生view会造成问题,就是输入框中长按然后左右移动时放大镜。
这个放大镜需要对下面的内容实时放大和位移,并以显示当前光标的具体位置。 这本来就很麻烦了,更麻烦是一旦此时实时在一套渲染中又做了形变操作,另一套渲染系统并不知道,就会错位。
上面5个混合渲染问题,可能在一个界面中组合出现,那就会造成更多兼容性问题和更差的性能。
有一个广泛流传的讹传。自渲染,是不是不同平台的UI一致性是严格一样的?原生渲染,是不是就无法保证不同平台的一致性?
真正开发过渲染引擎的人才知道,自渲染在不同平台要做很多原生适配工作。
为了和原生应用贴近,flutter等自渲染框架需要查看开源系统的相关算法,或者推测闭源系统的相关算法,在不同平台进行模拟。
一旦OS系统升级,修改了相关算法,flutter就得尽快更新,开发者也得尽快更新内嵌的flutter sdk。
Android有很多ROM厂商,如果有ROM厂商自己调整了滚动和回弹算法,flutter是难以适配这些个性化ROM的。
在一个App里,如果部分界面使用flutter渲染,这个问题会尤其严重,使用者会感觉到这个界面的滚动和回弹有点怪。
flutter的输入框,并不是系统的输入框控件,那个闪烁的光标是flutter引擎自己绘制的,为了让光标的大小、颜色、闪烁频率和系统保持一致,flutter都需要做平台适配。
输入框点击后,又会有更多问题
这都是flutter需要在每个平台都自行写代码处理的。这些代码,也都在消耗系统资源。
flutter和系统预置输入法的适配一般没有问题。 而三方输入法如果实现的很规范,也不会有问题,但仍然可能出现三方输入法在系统输入框下正常,但在flutter的输入框中异常的情况。 这很大程度上取决于三方输入法在发版时是否同时测试了flutter输入框的兼容性。
现代 Flutter 已经通过 Impeller 等方案显著改善了渲染后端的一致性和着色器问题。
但 Android 设备仍然存在大量 GPU 和驱动差异。
尤其是老设备、不同 GPU 厂商以及不同 Android 版本之间,图形能力并不完全一致。
因此,自渲染引擎依然需要进行大量兼容性测试。
也存在在某些边缘设备上GPU适配有问题的可能。
原生渲染效果在各平台不一致,这是react native的一些内置组件的特点,并不是所有原生渲染系统的共性。
造成这种差异的核心,不是原生渲染系统的问题,而是原生组件的实现方式问题。
flutter的widget,是一套dart代码编写的。而react native的不同组件,几乎都是各平台原生语言写的。
在 uni-app x 蒸汽模式中,通过相同的组件代码,实现了原生渲染下的UI一致性。这些组件使用跨平台的c++和uts语言编写。(并不要求开发者使用uts,开发者可使用js/ts)
这种方式,即保障了跨平台的严格一致性,又大幅提升了渲染性能。
同时 uni-app x 中使用系统原生组件也是无缝的,比如 uni-app x 内置的 glass-effect-view组件,就是iOS原生的液态玻璃组件。
几种不同的架构总结到下表中:
| 架构 | 渲染管线 | 组件和排版 | 典型代表 |
|---|---|---|---|
| 经典原生 | OS | OS native | UIKit / Android View |
| 原生渲染的框架UI | OS | 框架 | Compose UI Android版 / uni-app x 蒸汽模式 |
| 自渲染 | 框架 | 框架 | Flutter 和 compose-multiplatform iOS版 |
我们钦佩flutter团队的工程能力和代码水平,尤其是在GPU着色器上的极致运用。
但如果这个世界出现了一个跨平台引擎:
那对于开发者,是一个更完美的方案。
uni-app x 蒸汽模式,正是基于这种思路设计的,并且成功实现了上述目标。
在之前公布的benchmark中显示, uni-app x 蒸汽模式在渲染大量view和text时,性能是原生同类组件的2~3倍。 在长列表、canvas、rich-text等各项测试中也展现出强于原生或其他跨平台相同功能组件的性能表现。
详见:
一些人可能难以相信 uni-app x 蒸汽模式的渲染性能这么快,但解释 uni-app x 渲染更快的原因,需要更多单独长文,本文主要讲述的是原生渲染和自渲染的原理和优劣。
如果开发者对为什么渲染快的原因感兴趣,可以阅读 uni-app x 蒸汽模式的文档