May
16
先看一下Android和iOS的GUI渲染流程
现代Android的GUI绘制流程,排除软件渲染(无硬件加速)的理想的普通GUI渲染流程如下:

1. 一个Android的View,会通过draw()调用到一系列属性与绘制方法(Canvas API),来完成一个View的绘制。
2. 但是实际上调用draw()方法,并不会立刻绘制,而是在执行完成之后,对当前ViewTree得到对应的RenderNode Tree,而Canvas调用转换为了绘制命令列表DrawCmd也被称为DisplayList。这些数据会发送到RenderThread中。
3. RenderThread是一个系统维护的App线程,这个线程消费RenderNode数据,调用Skia与其GPU后端实现,完成每个View的内容的绘制,与View之间的组合,渲染出整个App的画面。也就是说每个App的RenderThread线程是每个App GUI渲染执行的地方。
4. 最后绘制的结果推入FrameBuffer,通知系统的SurfaceFlinger进程去消费数据。这个进程会将各个进程当前展示的元素,以Surface为单位组合在一起,最终呈现给用户。
注意到:
1. 一个View的内容的绘制,是以DrawCmd的形式,交给Skia去实际执行绘制的,并且Skia可以通过GPU后端进行绘制。Skia与GPU是View内容的实际绘制的实现。
2. 而View内容之间的叠加和组合,也就是RenderThread中对RenderNode本身的处理,最终也是调用Skia与对应的GPU实现的。同时也就是说View的Content绘制与View的合成同时在RenderThread进行的。
那么对应iOS的流程看起来是这样的:

1. 一个iOS的View,不管是UIKit还是SwiftUI,最终都通过CoreAnimation的CALayer的形式转换为RenderTree,最终以CATransaction的形式提交到系统的CoreAnimation.RenderServer进程进行渲染。
2. 其中一个View的内容的渲染并没有提交出去,而是在drawRect或者类似的私有方法中,调用CoreGraphics/CoreText直接完成的,并且将渲染位图作为CALayer的content数据发送出去。
3. 系统的RenderServer会收集各个App进程提交过来的Transaction,将所有的CALayer内容之间的叠加和组合和剪枝,同时也负责一些CALayer本身的属性与特效处理,最终展示到屏幕上。
注意到:
1. 一个View的内容的绘制总是通过CoreGraphics/CoreText这类的库在App中完成的,最终以图像的形式交出去。其中涉及的库,包括和skia类似的2d绘制库CoreGraphics只有软件实现,因此这个过程总是在CPU中完成的,也是在App进程的主线程完成的。
2. 而整个App本身的内容绘制 / 合成全交由RenderServer在系统进程中处理,RenderServer的下层是metal的GPU/硬件实现,因此App界面是集中在RenderServer进程渲染,并且直接上屏的。
3. 也就是说View的Content绘制是在主线程完成的而View的合成是在RenderServer进程完成的。
统一渲染与定性优点
进行双端对比,我们就可以为统一渲染定性:
- App界面的最终硬件渲染是发生在App进程中 还是 操作系统进程集中进行处理,这个区别即 分离渲染(Android) 与 统一渲染(iOS)。
- Android这样,应用渲染由应用进程管理,系统渲染服务只负责合成图层的方式叫做分离渲染。
- iOS这样,同时包括鸿蒙,应用的的渲染与图层合成都交由系统渲染服务的方式叫做统一渲染。
借用鸿蒙的图:

既然目前主流3个移动端中只有Android不是统一渲染,那我们可以直观列举出iOS与鸿蒙中的优势:
1. 全局渲染剪枝优化策略:直观的如上图,如果说Android的RenderThread可以以Window为单位,对App内的GUI渲染进行剪枝优化。统一渲染则可以根据rendertree,对整个屏幕多个进程之间的全部GUI整体进行控件(RenderNode)级别的遮挡剔除/剪枝,以实现渲染最优。
- 虽然对普通的App独立在前台的场景来说看起来问题不大,但是在移动端大屏多窗口的趋势下,问题会放大。
2. 更小的渲染Buffer(内存or显存)和通信开销:显然像Android那样先将App的内容独立绘制一次,在交给系统核心的过程需要额外为每一个surface开辟一块frame buffer进行一个独立的渲染来回。用iOS的话说每一帧都有一次全屏的离屏渲染。这一块额外的渲染性能消耗不可小觑。
- iOS如果要体验相同的效果可以在系统辅助设置中开启全屏反色,即是一次全屏的离屏渲染(Post-Screen Shader),中低端机可以肉眼可见的变得更卡顿。
3. 可以实现跨进程/跨surface的GUI效果:将所有进程和surface的RenderNode汇总在一起进行渲染,显然如果属性设置得当,就可以跨进程/跨surface的实现颜色混合 / shader类的视觉效果了。
- 典型的例子是iOS的毛玻璃效果。如下图桌面程序(springboard/backboard)与小控件分属不同的进程。但是小控件提交的根RenderNode的背景具有backdrop可以投射下方渲染结果的图像以实现毛玻璃效果。统一渲染即可最终混合出这个毛玻璃效果。如果是Android即整个widget是一个独立的surface独立完成渲染,则很难实现类似效果。

- 不过作为补充,现代Android系统的厂商ROM的会针对桌面场景的典型case进行优化,典型的场景就是壁纸进程和桌面进程的高斯模糊效果,经过优化后可以逼近统一渲染的效果但是性能会微差。
4. 可以实现更流畅的动画效果:现代GUI框架中,除了静态的RenderTree,每个node上的GUI动画也会以序列化数据的形式传递到渲染侧。动画在RenderServer中执行可以摆脱App性能的影响,系统统一调度更流畅的执行。
- 在iOS中偶尔可以看到App卡得不行甚至卡死了,但是上面的动画效果还在很流畅的循环,即是这个原因。
5. App本身的资源占用更轻:显然如果不在App内进行任何绘制行为,App进程纬度的内存显存开销都相当可控,一个仅GUI组成的App的资源占用相当轻,也就更有利于操作系统针对App进行资源调度 / 后台任务调度 / 墓碑机制等。
- 在iOS和鸿蒙中Flutter / CMP官方等自渲染跨平台方案,会导致应用资源占用劣化的原因也在于此。直接在App中利用skia进行渲染便是一个典型的分离渲染架构。相对于原生App的资源占用的劣势为对App资源分配带来严重的影响。
- 因此平台CMP会选择在iOS与鸿蒙上向平台渲染迁移,本质上是向统一渲染迁移。
整体来说,以上的优点汇总成一句话就是:统一渲染性能更高效的实现更丰富的效果(看起来)。
当然有优点,多少也会有缺点。从上面这个描述中我们可以听出一二:
1. 统一渲染进程RenderServer可能会是性能瓶颈:原本在每个App进程中完成的渲染任务统一到一个系统进程中了,原本在App渲染中可能参数的卡顿掉帧现在是一个全局的影响了。RenderServer反而可能成为单点可用性风险点。
2. 获取GUI中间渲染结果的成本更高:如果我们绘制的结果并不是要直接上屏,而是希望获取到Bitmap做一些处理进行储存或者再次上屏。Android原本调一次draw在进程内就可以解决的事情,在统一渲染的操作系统中会是一个需要IPC跨进程通信并且有大量数据传输的高成本操作,因此相关逻辑的自由度会大打折扣,也同时会影响实际操作系统Framework提供能力的设计。
这些缺点并不完全是枷锁,而是成为指导统一渲染演进的方向。
iOS中的统一渲染特征
当然,iOS的这个统一渲染并不纯粹,虽然大体上可以定义为统一渲染,但是说是它是一个混合渲染会更合适。回到这张图:

相对于Android,iOS的渲染过程中底层渲染与skia收敛的状态相比,那可是百花齐放一大堆的东西了。经过整体可以这样看:

- 相对于Android绘制层统一为一个绘制引擎(目前主要为skia),该引擎内部抹平CPU渲染与GPU渲染差异。iOS用户可以直接接触到的和GUI绘制直接相关的library就有一大堆,主要为——CoreGraphics / CoreText / CoreAnimation
- 其中CoreGraphics / CoreText组成的是传统的看起来对标Canvas的API:进行图形和文字绘制。看起来也是和skia对标的。但是不同的是,iOS中的这两者只有CPU后端实现。也就是说,性能堪忧。
- 而CoreAnimation虽然从名字上看只是动画相关库,它实际上是真正的GUI底层绘制框架(类似于HWUI在Android中的地位)。并且由于统一渲染的特征,实际CoreAnimation可以区分为客户端和渲染端,客户端即开发者感知到这部分,渲染端就是RenderServer了。CoreAnimation相关的东西总是尽可能的GPU实现。
- 可以注意到,实际上iOS将渲染的CPU实现部分和GPU实现部分通过不同的图形library给区分开了。
- 并且CPU实现部分仍然是在App内执行。
- 而GPU实现的部分则是统一渲染,在renderServer中执行。
- 另外从选择的职责上看,感知到iOS渲染架构的分割。对于一个RenderNode(感知为CALayer):
- Node的业务内容的渲染即content,默认总是在App进程内CPU渲染的,如文本(UILabel中的文本利用CoreText绘制),特殊图形(UISwitch部分元素利用CoreGraphics绘制),最终以图像的形式输出。
- 而Node的基础绘制能力如背景 / 圆角/阴影/边缘 / 位移形变 / 渐变染色特效 / 高斯模糊,这类则是由CoreAnimation交给GPU处理,在统一渲染层处理。
- 当然各个Node之间的合成也是在统一渲染层GPU处理的。
优缺点与特殊Case?
1. FAQ:考虑到Android渲染全程(包括图形与文字)都是走的Skia并且大概率GPU后端可用,也就是说Android GUI内容渲染效率比iOS高?
- 对于CoreText负责的部分,即使是Skia的GPU后端可用,整个渲染过程中也有大量不得不使用CPU渲染的部分。文本就是这样,几乎不会有渲染方案会真的将文本以矢量图形的方式投入GPU,一定是渲染为位图skia也不例外(类似于FreeType干的事儿)。因此对于文本渲染来说,可以认为没有性能差距。
- 而对于CoreGraphics负责的部分,实际上排除上文描述的“Node的基础绘制能力”包含的渲染能力,一个GUI界面中真的会利用绘制能力去完成的内容少之又少,所以此处确实会有性能差距,但是涉及的工作量少也就意味着——不明显。
2. FAQ:如果iOS对标Canvas的能力CoreGraphics只有CPU渲染,在真需要复杂绘制的场景岂不是性能很差?
- 是也不是,iOS中对标Canvas的API确实只有CPU渲染性能堪忧,甚至不如浏览器canvas。
- 但是CoreGraphics能干的事情,绝大部分在CoreAnimation能找到平替。比如如果绘制一个多边形,在CoreAnimation中可以利用CAShapeLayer这样的特定RenderNode来完成。唯一的问题是CoreAnimation看起来不像绘制API,而是元素组合起来的。
- 一个GoodCase是,iOS的lottie库,旧版本是用CoreGraphics实现的,性能确实堪忧。而22年Lottie库进行了重构以非常轻微的breakchange,迁入CoreAnimation渲染实现,从而实现GPU渲染。如果Lottie都可以做到,可以认为对于绝大部分场景如果追求性能不会有阻碍。
3. FAQ:为什么iOS会将CPU渲染部分留在App进程中,而不是一同迁入统一渲染进程中?
- iOS系统源于macOS,显然macOS也是从分离渲染逐步演化成统一渲染,从CPU转向GPU的,而在这个过程中,历史macOS的设计拆分保留了下来。无论如何这是这个事儿的起点。
- 同时很显然的,相较于GPU渲染,少量的CPU渲染本身的资源占用成本并不高,并且相较于GUI渲染,纯图像绘制在CPU渲染的使用面更大,在这时候刻意将少量多次的CPU渲染也放入统一渲染进程反而增加了业务代码通信耗时与资源占用。同时也会加剧前文所说的RenderServer性能瓶颈问题。
- 从结果来看,目前iOS系统上这个职责的分担和平衡相对来说是合理的,App的主线程和RenderService均匀的在两个tick中承担责任,保障GUI渲染的流畅度。
- 当然,RenderServer不提供基于drawing cmd的统一渲染,也成为了flutter / cmp这类基于skia的自渲染引擎跨端方案向统一渲染迁移的阻碍。
- 以cmp向统一渲染迁移为例,在鸿蒙上,鸿蒙的renderservice是提供了直接处理drawcmd的能力的,使得自渲染引擎只需要较为低成本的API迁移和适配,就可以完成平台统一渲染的迁移。
- 而到iOS上,自渲染调用skia几乎没法直接映射到操作系统提供的以上能力上,需要一个成本非常高昂的封装层,将各种CoreAnimation / CoreGraphics / CoreText提供的能力按照最优性能与合理性重新封装成drawing能力,并且由于API风格与能力的差异不可避免的存在平台break与性能损耗。
4. FAQ:有什么GUI能力/效果是iOS的统一渲染是做不到的?
- 对比Android与iOS GUI体系中的能力,目前有一项Android一直以来存在GUI能力iOS难以做到——即自定义GUI Shader效果。Android可以通过RenderEffect的createRuntimeShaderEffect,在RenderNode的渲染流水线上应用自定义Shader。而iOS的CALayer,只能使用固定的数个CAFliter效果,无法加载程序中的自定义shader。
- 理由也很简单,跨进程的让操作系统进程加载用户App携带的shader,成本高并且有严重的可用性/安全隐患,不太可能开放这个口子。而在用户进程中随便加载当然没问题。
- 当然能利用特殊的流程Case来实现。iOS17开始,苹果在SwiftUI中利用ShaderLibrary实现了类似的效果。我们知道SwiftUI也是基于CoreAnimation的,因此可以逆向出这个渲染过程:

- 对于一个需要Apply自定义shader的RenderNode(RenderBox Layer),进入Node绘制流程时,node先将自己的childnode,投入统一渲染走一遭,得到自己的content的渲染结果,然后又将这张图在App进程中投入该shader生成的GPU渲染的流水线中,得到第二个bitmap。这个bitmap作为最终的node content,再继续正常的GUI流程。——成本如此之高的一个流程才能实现。
- 如果是Android的RenderEffect的话,或者是skia的SkRuntimeEffect,仅仅就是一次普通的离屏渲染而已。(等效于其它iOS CALayer内置效果,如filter = .blur便是如下图)

鸿蒙中的统一渲染特征
来到鸿蒙,鸿蒙本质上也是在操作系统迭代过程中,从简单到复杂,从分离渲染迁移到统一渲染的。
目前主流移动端的鸿蒙的GUI渲染过程简单描述,就是和Android一模一样,然后把RenderThread搬家到RenderService,以完成统一渲染。官方图如下:

- 同时鸿蒙的GUI图像渲染层是完全开源的,可以深入一二 https://github.com/openharmony/graphic_graphic_2d
如果本文档的图为例子,和Android进行对应,不严谨的简化再简化看起来会是这样:

很直观可以看到:
1. 作为统一渲染,相对于Android需要RenderThread处理一次后SurfaceFlinger再处理一次,鸿蒙中RenderService,一口气把渲染和合成同时完成了。如上文所说避免了额外的FrameBuffer消耗。
2. 当然在这同时,整体传输数据方式 / draw方式 和 底层的skia,架构上保持和Android一致,同时也意味着鸿蒙整体代码可进可退:可以在鸿蒙系统上统一渲染,也可以在ArkUI-X中作为分离渲染的渲染引擎运行在其它操作系统中,也可以让鸿蒙系统的GUI在各种模式下以适配更多的中低端设备。
3. 相对于iOS来说,也就是说所有的渲染任务,包括content draw / node draw / compositing都是在RenderService中完成的。更统一也更重了。
- 相较于iOS的RenderServer只消费RenderNode与渲染完成的bitmap,鸿蒙的RenderService接受RenderNode的同时也接受纯粹的DrawCmd,也就是说在鸿蒙中,纯粹的Canvas Drawing API调用也可以利用统一渲染能力进行调度,以减少App的资源占用——这也就是上文所说自渲染跨端渲染引擎可平台实现的基础。
- 可以想象为,iOS在纯粹的调用CoreGraphics进行异步绘制时也能从统一GPU渲染获得收益。
- 这件事原理上Apple也可以做,WebKit中Web Canvas的硬件加速同样也是跨进程的。但是目前苹果不愿意在GUI做如此大的改变。
性能优化视角,我们并不知道iOS的renderServer到底做了什么优化,但是鸿蒙的信息是公开的,拥有比iOS更统一任务负担更重的统一渲染层的鸿蒙系统确实在这部分做了相当多的优化。
- 核心优化是全渲染流水线并行化。由于RenderService同时要进行业务逻辑上的content draw / node draw等多个纬度的事情,自然不可避免的会存在大规模的离屏渲染与渲染结果的前后依赖。因此鸿蒙渲染服务引入了ParallelRender机制。
- 当然这个机制的同时拥有的基础是skia整体vulkan化。通过彻底的对skia的Vulkan并行化多线程改造,可以各个线程同时录制各个子树的command buffer(DrawCmd)。再统一到主task任务做合并到一个buffer中,统一提交给GPU。

- 同时,RenderService中Prepare等其他环节也一并进行并行化优化。实际RenderService中的线程模型从主线程到渲染线程到硬件线程会更复杂。
还有什么优缺点与特殊Case?
1. FAQ:有没有什么和iOS一样统一渲染共同的缺点?
- 最显著的缺点和iOS一样,鸿蒙的绘制引擎目前没有支持自定义shader(像iOS一样effectKit模块只提供了相对固定的效果)。
- 不过相对于iOS,整体都通过skia实现鸿蒙完全可以利用sksl(Skia Shading Language)作为一个安全的中间层,来跨进程的进行shader编译与运行。
- 另外最不济的情况下,经过多次通信也是可以解决自定义shader问题的,如iOS那样。另外如果对于安全的管控更宽松一些,在系统进程以插件的形式以来用户提供的shader也是可行的选项。
2. FAQ:需要ipc通信的统一渲染的纯Canvas API会不会有什么缺点?
- 虽然相较于Android与iOS现状的都是App类渲染,确实会有耗时风险。但是考虑到 1. 纯canvas不上屏的场景也就是对性能没有要求,自然不用在意ipc通信时间。2. 同时对标浏览器方案,如上文浏览器canvas硬件加速也会跨进程通信,考虑到鸿蒙的实际业务层是JS通过NAPI调用,最不济也就是一个浏览器的调用流程。因此整体来看风险可控可接受。
- 同时在CMP的平台迁移过程中,实际测试下相对于自渲染的进程内分离渲染方案,迁移到平台渲染确实从结果上获得的用户感知层的流畅性优势,因此从结论上也是优化的。
趋势展望
抛开Android与iOS的现状,未来各种设备的硬件渲染能力也就是GPU或者专有硬件渲染能力会越来越强,因此硬件渲染必然是将来的趋势。同时其实可以关注到,统一渲染本身也是随着软硬件结合能力的增强,同时类似于苹果这样的操作系统对于GUI渲染层强一体化建设,才逐步走到前台的。
统一渲染通过集中化的渲染管理与跨进程资源整合,实现更高性能、更低资源消耗、更丰富的视觉效果,再加上事实上鸿蒙的迁移,无论如何它都是GUI的未来。
因此在可以预期的未来:
- To C强调强用户体验的PC移动端操作系统,提供的现代GUI方案,会继续向硬件渲染与统一渲染靠拢。在各个操作系统上实现高帧率GUI渲染的性能突破。
- 在统一渲染的框架下,继续对单帧渲染耗时进行优化,除了继续大面积并行,可能的方向还包括更激进甚至结合AI与预测的渲染剪枝技术,更现代的内存/显存共享与零拷贝通信技术等。
- 类似于Skia,业界诞生出更统一通用的渲染 / 绘制能力层,定义跨进程渲染命令格式,对跨平台技术与其用户体验更加友好。
- 同时更丰富的GUI / 视觉 / 动画能力也是统一渲染技术未来能提供给我们的。
统一渲染这样GUI架构的复杂度的变高并不一定是坏事,算力的增长配合更丰富的呈现能力同时结合更闭环的优化方案,最终在终端发展的趋势下给到用户更优秀的用户体验。
参考文章:
以鸿蒙为例,详解统一渲染技术
OpenHarmony 鸿蒙 3.2版本渲染架构及优化总结
深入理解 iOS Rendering Process
现代Android的GUI绘制流程,排除软件渲染(无硬件加速)的理想的普通GUI渲染流程如下:
1. 一个Android的View,会通过draw()调用到一系列属性与绘制方法(Canvas API),来完成一个View的绘制。
2. 但是实际上调用draw()方法,并不会立刻绘制,而是在执行完成之后,对当前ViewTree得到对应的RenderNode Tree,而Canvas调用转换为了绘制命令列表DrawCmd也被称为DisplayList。这些数据会发送到RenderThread中。
3. RenderThread是一个系统维护的App线程,这个线程消费RenderNode数据,调用Skia与其GPU后端实现,完成每个View的内容的绘制,与View之间的组合,渲染出整个App的画面。也就是说每个App的RenderThread线程是每个App GUI渲染执行的地方。
4. 最后绘制的结果推入FrameBuffer,通知系统的SurfaceFlinger进程去消费数据。这个进程会将各个进程当前展示的元素,以Surface为单位组合在一起,最终呈现给用户。
注意到:
1. 一个View的内容的绘制,是以DrawCmd的形式,交给Skia去实际执行绘制的,并且Skia可以通过GPU后端进行绘制。Skia与GPU是View内容的实际绘制的实现。
2. 而View内容之间的叠加和组合,也就是RenderThread中对RenderNode本身的处理,最终也是调用Skia与对应的GPU实现的。同时也就是说View的Content绘制与View的合成同时在RenderThread进行的。
那么对应iOS的流程看起来是这样的:
1. 一个iOS的View,不管是UIKit还是SwiftUI,最终都通过CoreAnimation的CALayer的形式转换为RenderTree,最终以CATransaction的形式提交到系统的CoreAnimation.RenderServer进程进行渲染。
2. 其中一个View的内容的渲染并没有提交出去,而是在drawRect或者类似的私有方法中,调用CoreGraphics/CoreText直接完成的,并且将渲染位图作为CALayer的content数据发送出去。
3. 系统的RenderServer会收集各个App进程提交过来的Transaction,将所有的CALayer内容之间的叠加和组合和剪枝,同时也负责一些CALayer本身的属性与特效处理,最终展示到屏幕上。
注意到:
1. 一个View的内容的绘制总是通过CoreGraphics/CoreText这类的库在App中完成的,最终以图像的形式交出去。其中涉及的库,包括和skia类似的2d绘制库CoreGraphics只有软件实现,因此这个过程总是在CPU中完成的,也是在App进程的主线程完成的。
2. 而整个App本身的内容绘制 / 合成全交由RenderServer在系统进程中处理,RenderServer的下层是metal的GPU/硬件实现,因此App界面是集中在RenderServer进程渲染,并且直接上屏的。
3. 也就是说View的Content绘制是在主线程完成的而View的合成是在RenderServer进程完成的。
统一渲染与定性优点
进行双端对比,我们就可以为统一渲染定性:
- App界面的最终硬件渲染是发生在App进程中 还是 操作系统进程集中进行处理,这个区别即 分离渲染(Android) 与 统一渲染(iOS)。
- Android这样,应用渲染由应用进程管理,系统渲染服务只负责合成图层的方式叫做分离渲染。
- iOS这样,同时包括鸿蒙,应用的的渲染与图层合成都交由系统渲染服务的方式叫做统一渲染。
借用鸿蒙的图:
既然目前主流3个移动端中只有Android不是统一渲染,那我们可以直观列举出iOS与鸿蒙中的优势:
1. 全局渲染剪枝优化策略:直观的如上图,如果说Android的RenderThread可以以Window为单位,对App内的GUI渲染进行剪枝优化。统一渲染则可以根据rendertree,对整个屏幕多个进程之间的全部GUI整体进行控件(RenderNode)级别的遮挡剔除/剪枝,以实现渲染最优。
- 虽然对普通的App独立在前台的场景来说看起来问题不大,但是在移动端大屏多窗口的趋势下,问题会放大。
2. 更小的渲染Buffer(内存or显存)和通信开销:显然像Android那样先将App的内容独立绘制一次,在交给系统核心的过程需要额外为每一个surface开辟一块frame buffer进行一个独立的渲染来回。用iOS的话说每一帧都有一次全屏的离屏渲染。这一块额外的渲染性能消耗不可小觑。
- iOS如果要体验相同的效果可以在系统辅助设置中开启全屏反色,即是一次全屏的离屏渲染(Post-Screen Shader),中低端机可以肉眼可见的变得更卡顿。
3. 可以实现跨进程/跨surface的GUI效果:将所有进程和surface的RenderNode汇总在一起进行渲染,显然如果属性设置得当,就可以跨进程/跨surface的实现颜色混合 / shader类的视觉效果了。
- 典型的例子是iOS的毛玻璃效果。如下图桌面程序(springboard/backboard)与小控件分属不同的进程。但是小控件提交的根RenderNode的背景具有backdrop可以投射下方渲染结果的图像以实现毛玻璃效果。统一渲染即可最终混合出这个毛玻璃效果。如果是Android即整个widget是一个独立的surface独立完成渲染,则很难实现类似效果。
- 不过作为补充,现代Android系统的厂商ROM的会针对桌面场景的典型case进行优化,典型的场景就是壁纸进程和桌面进程的高斯模糊效果,经过优化后可以逼近统一渲染的效果但是性能会微差。
4. 可以实现更流畅的动画效果:现代GUI框架中,除了静态的RenderTree,每个node上的GUI动画也会以序列化数据的形式传递到渲染侧。动画在RenderServer中执行可以摆脱App性能的影响,系统统一调度更流畅的执行。
- 在iOS中偶尔可以看到App卡得不行甚至卡死了,但是上面的动画效果还在很流畅的循环,即是这个原因。
5. App本身的资源占用更轻:显然如果不在App内进行任何绘制行为,App进程纬度的内存显存开销都相当可控,一个仅GUI组成的App的资源占用相当轻,也就更有利于操作系统针对App进行资源调度 / 后台任务调度 / 墓碑机制等。
- 在iOS和鸿蒙中Flutter / CMP官方等自渲染跨平台方案,会导致应用资源占用劣化的原因也在于此。直接在App中利用skia进行渲染便是一个典型的分离渲染架构。相对于原生App的资源占用的劣势为对App资源分配带来严重的影响。
- 因此平台CMP会选择在iOS与鸿蒙上向平台渲染迁移,本质上是向统一渲染迁移。
整体来说,以上的优点汇总成一句话就是:统一渲染性能更高效的实现更丰富的效果(看起来)。
当然有优点,多少也会有缺点。从上面这个描述中我们可以听出一二:
1. 统一渲染进程RenderServer可能会是性能瓶颈:原本在每个App进程中完成的渲染任务统一到一个系统进程中了,原本在App渲染中可能参数的卡顿掉帧现在是一个全局的影响了。RenderServer反而可能成为单点可用性风险点。
2. 获取GUI中间渲染结果的成本更高:如果我们绘制的结果并不是要直接上屏,而是希望获取到Bitmap做一些处理进行储存或者再次上屏。Android原本调一次draw在进程内就可以解决的事情,在统一渲染的操作系统中会是一个需要IPC跨进程通信并且有大量数据传输的高成本操作,因此相关逻辑的自由度会大打折扣,也同时会影响实际操作系统Framework提供能力的设计。
这些缺点并不完全是枷锁,而是成为指导统一渲染演进的方向。
iOS中的统一渲染特征
当然,iOS的这个统一渲染并不纯粹,虽然大体上可以定义为统一渲染,但是说是它是一个混合渲染会更合适。回到这张图:
相对于Android,iOS的渲染过程中底层渲染与skia收敛的状态相比,那可是百花齐放一大堆的东西了。经过整体可以这样看:
- 相对于Android绘制层统一为一个绘制引擎(目前主要为skia),该引擎内部抹平CPU渲染与GPU渲染差异。iOS用户可以直接接触到的和GUI绘制直接相关的library就有一大堆,主要为——CoreGraphics / CoreText / CoreAnimation
- 其中CoreGraphics / CoreText组成的是传统的看起来对标Canvas的API:进行图形和文字绘制。看起来也是和skia对标的。但是不同的是,iOS中的这两者只有CPU后端实现。也就是说,性能堪忧。
- 而CoreAnimation虽然从名字上看只是动画相关库,它实际上是真正的GUI底层绘制框架(类似于HWUI在Android中的地位)。并且由于统一渲染的特征,实际CoreAnimation可以区分为客户端和渲染端,客户端即开发者感知到这部分,渲染端就是RenderServer了。CoreAnimation相关的东西总是尽可能的GPU实现。
- 可以注意到,实际上iOS将渲染的CPU实现部分和GPU实现部分通过不同的图形library给区分开了。
- 并且CPU实现部分仍然是在App内执行。
- 而GPU实现的部分则是统一渲染,在renderServer中执行。
- 另外从选择的职责上看,感知到iOS渲染架构的分割。对于一个RenderNode(感知为CALayer):
- Node的业务内容的渲染即content,默认总是在App进程内CPU渲染的,如文本(UILabel中的文本利用CoreText绘制),特殊图形(UISwitch部分元素利用CoreGraphics绘制),最终以图像的形式输出。
- 而Node的基础绘制能力如背景 / 圆角/阴影/边缘 / 位移形变 / 渐变染色特效 / 高斯模糊,这类则是由CoreAnimation交给GPU处理,在统一渲染层处理。
- 当然各个Node之间的合成也是在统一渲染层GPU处理的。
优缺点与特殊Case?
1. FAQ:考虑到Android渲染全程(包括图形与文字)都是走的Skia并且大概率GPU后端可用,也就是说Android GUI内容渲染效率比iOS高?
- 对于CoreText负责的部分,即使是Skia的GPU后端可用,整个渲染过程中也有大量不得不使用CPU渲染的部分。文本就是这样,几乎不会有渲染方案会真的将文本以矢量图形的方式投入GPU,一定是渲染为位图skia也不例外(类似于FreeType干的事儿)。因此对于文本渲染来说,可以认为没有性能差距。
- 而对于CoreGraphics负责的部分,实际上排除上文描述的“Node的基础绘制能力”包含的渲染能力,一个GUI界面中真的会利用绘制能力去完成的内容少之又少,所以此处确实会有性能差距,但是涉及的工作量少也就意味着——不明显。
2. FAQ:如果iOS对标Canvas的能力CoreGraphics只有CPU渲染,在真需要复杂绘制的场景岂不是性能很差?
- 是也不是,iOS中对标Canvas的API确实只有CPU渲染性能堪忧,甚至不如浏览器canvas。
- 但是CoreGraphics能干的事情,绝大部分在CoreAnimation能找到平替。比如如果绘制一个多边形,在CoreAnimation中可以利用CAShapeLayer这样的特定RenderNode来完成。唯一的问题是CoreAnimation看起来不像绘制API,而是元素组合起来的。
- 一个GoodCase是,iOS的lottie库,旧版本是用CoreGraphics实现的,性能确实堪忧。而22年Lottie库进行了重构以非常轻微的breakchange,迁入CoreAnimation渲染实现,从而实现GPU渲染。如果Lottie都可以做到,可以认为对于绝大部分场景如果追求性能不会有阻碍。
3. FAQ:为什么iOS会将CPU渲染部分留在App进程中,而不是一同迁入统一渲染进程中?
- iOS系统源于macOS,显然macOS也是从分离渲染逐步演化成统一渲染,从CPU转向GPU的,而在这个过程中,历史macOS的设计拆分保留了下来。无论如何这是这个事儿的起点。
- 同时很显然的,相较于GPU渲染,少量的CPU渲染本身的资源占用成本并不高,并且相较于GUI渲染,纯图像绘制在CPU渲染的使用面更大,在这时候刻意将少量多次的CPU渲染也放入统一渲染进程反而增加了业务代码通信耗时与资源占用。同时也会加剧前文所说的RenderServer性能瓶颈问题。
- 从结果来看,目前iOS系统上这个职责的分担和平衡相对来说是合理的,App的主线程和RenderService均匀的在两个tick中承担责任,保障GUI渲染的流畅度。
- 当然,RenderServer不提供基于drawing cmd的统一渲染,也成为了flutter / cmp这类基于skia的自渲染引擎跨端方案向统一渲染迁移的阻碍。
- 以cmp向统一渲染迁移为例,在鸿蒙上,鸿蒙的renderservice是提供了直接处理drawcmd的能力的,使得自渲染引擎只需要较为低成本的API迁移和适配,就可以完成平台统一渲染的迁移。
- 而到iOS上,自渲染调用skia几乎没法直接映射到操作系统提供的以上能力上,需要一个成本非常高昂的封装层,将各种CoreAnimation / CoreGraphics / CoreText提供的能力按照最优性能与合理性重新封装成drawing能力,并且由于API风格与能力的差异不可避免的存在平台break与性能损耗。
4. FAQ:有什么GUI能力/效果是iOS的统一渲染是做不到的?
- 对比Android与iOS GUI体系中的能力,目前有一项Android一直以来存在GUI能力iOS难以做到——即自定义GUI Shader效果。Android可以通过RenderEffect的createRuntimeShaderEffect,在RenderNode的渲染流水线上应用自定义Shader。而iOS的CALayer,只能使用固定的数个CAFliter效果,无法加载程序中的自定义shader。
- 理由也很简单,跨进程的让操作系统进程加载用户App携带的shader,成本高并且有严重的可用性/安全隐患,不太可能开放这个口子。而在用户进程中随便加载当然没问题。
- 当然能利用特殊的流程Case来实现。iOS17开始,苹果在SwiftUI中利用ShaderLibrary实现了类似的效果。我们知道SwiftUI也是基于CoreAnimation的,因此可以逆向出这个渲染过程:
- 对于一个需要Apply自定义shader的RenderNode(RenderBox Layer),进入Node绘制流程时,node先将自己的childnode,投入统一渲染走一遭,得到自己的content的渲染结果,然后又将这张图在App进程中投入该shader生成的GPU渲染的流水线中,得到第二个bitmap。这个bitmap作为最终的node content,再继续正常的GUI流程。——成本如此之高的一个流程才能实现。
- 如果是Android的RenderEffect的话,或者是skia的SkRuntimeEffect,仅仅就是一次普通的离屏渲染而已。(等效于其它iOS CALayer内置效果,如filter = .blur便是如下图)
鸿蒙中的统一渲染特征
来到鸿蒙,鸿蒙本质上也是在操作系统迭代过程中,从简单到复杂,从分离渲染迁移到统一渲染的。
目前主流移动端的鸿蒙的GUI渲染过程简单描述,就是和Android一模一样,然后把RenderThread搬家到RenderService,以完成统一渲染。官方图如下:
- 同时鸿蒙的GUI图像渲染层是完全开源的,可以深入一二 https://github.com/openharmony/graphic_graphic_2d
如果本文档的图为例子,和Android进行对应,不严谨的简化再简化看起来会是这样:
很直观可以看到:
1. 作为统一渲染,相对于Android需要RenderThread处理一次后SurfaceFlinger再处理一次,鸿蒙中RenderService,一口气把渲染和合成同时完成了。如上文所说避免了额外的FrameBuffer消耗。
2. 当然在这同时,整体传输数据方式 / draw方式 和 底层的skia,架构上保持和Android一致,同时也意味着鸿蒙整体代码可进可退:可以在鸿蒙系统上统一渲染,也可以在ArkUI-X中作为分离渲染的渲染引擎运行在其它操作系统中,也可以让鸿蒙系统的GUI在各种模式下以适配更多的中低端设备。
3. 相对于iOS来说,也就是说所有的渲染任务,包括content draw / node draw / compositing都是在RenderService中完成的。更统一也更重了。
- 相较于iOS的RenderServer只消费RenderNode与渲染完成的bitmap,鸿蒙的RenderService接受RenderNode的同时也接受纯粹的DrawCmd,也就是说在鸿蒙中,纯粹的Canvas Drawing API调用也可以利用统一渲染能力进行调度,以减少App的资源占用——这也就是上文所说自渲染跨端渲染引擎可平台实现的基础。
- 可以想象为,iOS在纯粹的调用CoreGraphics进行异步绘制时也能从统一GPU渲染获得收益。
- 这件事原理上Apple也可以做,WebKit中Web Canvas的硬件加速同样也是跨进程的。但是目前苹果不愿意在GUI做如此大的改变。
性能优化视角,我们并不知道iOS的renderServer到底做了什么优化,但是鸿蒙的信息是公开的,拥有比iOS更统一任务负担更重的统一渲染层的鸿蒙系统确实在这部分做了相当多的优化。
- 核心优化是全渲染流水线并行化。由于RenderService同时要进行业务逻辑上的content draw / node draw等多个纬度的事情,自然不可避免的会存在大规模的离屏渲染与渲染结果的前后依赖。因此鸿蒙渲染服务引入了ParallelRender机制。
- 当然这个机制的同时拥有的基础是skia整体vulkan化。通过彻底的对skia的Vulkan并行化多线程改造,可以各个线程同时录制各个子树的command buffer(DrawCmd)。再统一到主task任务做合并到一个buffer中,统一提交给GPU。
- 同时,RenderService中Prepare等其他环节也一并进行并行化优化。实际RenderService中的线程模型从主线程到渲染线程到硬件线程会更复杂。
还有什么优缺点与特殊Case?
1. FAQ:有没有什么和iOS一样统一渲染共同的缺点?
- 最显著的缺点和iOS一样,鸿蒙的绘制引擎目前没有支持自定义shader(像iOS一样effectKit模块只提供了相对固定的效果)。
- 不过相对于iOS,整体都通过skia实现鸿蒙完全可以利用sksl(Skia Shading Language)作为一个安全的中间层,来跨进程的进行shader编译与运行。
- 另外最不济的情况下,经过多次通信也是可以解决自定义shader问题的,如iOS那样。另外如果对于安全的管控更宽松一些,在系统进程以插件的形式以来用户提供的shader也是可行的选项。
2. FAQ:需要ipc通信的统一渲染的纯Canvas API会不会有什么缺点?
- 虽然相较于Android与iOS现状的都是App类渲染,确实会有耗时风险。但是考虑到 1. 纯canvas不上屏的场景也就是对性能没有要求,自然不用在意ipc通信时间。2. 同时对标浏览器方案,如上文浏览器canvas硬件加速也会跨进程通信,考虑到鸿蒙的实际业务层是JS通过NAPI调用,最不济也就是一个浏览器的调用流程。因此整体来看风险可控可接受。
- 同时在CMP的平台迁移过程中,实际测试下相对于自渲染的进程内分离渲染方案,迁移到平台渲染确实从结果上获得的用户感知层的流畅性优势,因此从结论上也是优化的。
趋势展望
抛开Android与iOS的现状,未来各种设备的硬件渲染能力也就是GPU或者专有硬件渲染能力会越来越强,因此硬件渲染必然是将来的趋势。同时其实可以关注到,统一渲染本身也是随着软硬件结合能力的增强,同时类似于苹果这样的操作系统对于GUI渲染层强一体化建设,才逐步走到前台的。
统一渲染通过集中化的渲染管理与跨进程资源整合,实现更高性能、更低资源消耗、更丰富的视觉效果,再加上事实上鸿蒙的迁移,无论如何它都是GUI的未来。
因此在可以预期的未来:
- To C强调强用户体验的PC移动端操作系统,提供的现代GUI方案,会继续向硬件渲染与统一渲染靠拢。在各个操作系统上实现高帧率GUI渲染的性能突破。
- 在统一渲染的框架下,继续对单帧渲染耗时进行优化,除了继续大面积并行,可能的方向还包括更激进甚至结合AI与预测的渲染剪枝技术,更现代的内存/显存共享与零拷贝通信技术等。
- 类似于Skia,业界诞生出更统一通用的渲染 / 绘制能力层,定义跨进程渲染命令格式,对跨平台技术与其用户体验更加友好。
- 同时更丰富的GUI / 视觉 / 动画能力也是统一渲染技术未来能提供给我们的。
统一渲染这样GUI架构的复杂度的变高并不一定是坏事,算力的增长配合更丰富的呈现能力同时结合更闭环的优化方案,最终在终端发展的趋势下给到用户更优秀的用户体验。
参考文章:
以鸿蒙为例,详解统一渲染技术
OpenHarmony 鸿蒙 3.2版本渲染架构及优化总结
深入理解 iOS Rendering Process