发布时间:2022-08-28 23:00
动手点关注 干货不迷路
计算机或手机的渲染是一个非常复杂的过程,本文介绍了渲染相关的一些基础知识,并结合 iOS 和安卓的技术框架介绍了移动端渲染原理,最后详细的解析了 iOS 中的离屏渲染以及圆角优化的一些方法。
我们在屏幕上绘制图像需要的原始数据叫做位图。位图(Bitmap) 是一种数据结构。一个位图是由 n*m 个像素组成,每个像素的颜色信息由 RGB 组合或者灰度值表示。根据位深度,可将位图分为 1、4、8、16、24 及 32 位图像等。每个像素使用的信息位数越多,可用的颜色就越多,颜色表现就越逼真,越丰富,相应的数据量越大。
位图一般存储的是物理像素,而应用层一般用的是逻辑像素,物理像素和逻辑像素之间会存在一定的对应关系。例如,iOS 中物理像素和逻辑像素的对应关系如下:
iOS1 倍屏 1pt 对应 1 个物理像素
iOS2 倍屏 1pt 对应 2 个物理像素
iOS3 倍屏 1pt 对应 3 个物理像素
上边讲了屏幕上绘制图像需要的原始数据叫做位图。那么问题来了,有了位图数据之后如何将图像绘制到屏幕上呢?如下图所示:电子枪从上到下逐行扫描,扫描完成后显示器就呈现一帧画面。然后电子枪回到屏幕初始位置进行下一次扫描。为了同步显示器的显示过程和视频控制器的扫描过程,显示器会用硬件时钟产生一系列的定时信号。当电子枪换行进行扫描时,显示器会发出一个水平同步信号;当一帧画面绘制完成后,电子枪回到原位,准备画下一帧前,显示器会发出一个垂直同步信号。显示器通常以固定的频率进行刷新,这个刷新率就是垂直同步信号产生的频率。
前一部分介绍了视频控制器将位图数据显示到物理屏幕上的过程,那么位图数据是怎么得到的呢?其实位图数据是通过 CPU、GPU 协同工作得到的。下图就是常见的 CPU、GPU、显示器协同工作的流程。CPU 计算好显示内容提交至 GPU,GPU 渲染完成后将渲染结果存入帧缓冲区,接下来就需要将得到的像素信息显示在物理屏幕上了,这时候视频控制器(Video Controller)会读取帧缓冲器中的信息传递给显示器(Monitor)进行显示。完整的流程如下图所示:
CPU 和 GPU 的区别
讲到 CPU、GPU、显示器的协同工作流程,就不得不提一下 CPU 和 GPU 的区别。
CPU是中央处理器,适合单一复杂逻辑,而GPU是图形处理器,适合高并发简单逻辑。
GPU 有特别多的的计算单元和超长的流水线,但是控制逻辑非常简单,并且还省去了缓存,适合对低延迟要求不高的运算。而 CPU 不仅被 Cache 占据了大量空间,而且还有特别复杂的控制逻辑,相比之下计算能力只是 CPU 很小的一部分。图形渲染涉及到的矩阵运算比较多,矩阵相关的运算可以被拆分成并行的简单的运算,所以渲染处理这件事特别适合 GPU 去做。
总结来说:GPU 的工作计算量大,但技术含量不高,需要简单重复很多次。就好比有个工作需要算成百上千次一百以内加减乘除一样。而 CPU 就像老教授,积分微分都会算,适合处理单一复杂逻辑运算。
我们通常将图像绘制的完整流程称为渲染流水线,这个过程是 CPU 和 GPU 协作完成的。一般一个渲染流程可以分成 4 个概念阶段,分别是:应用阶段(Application Stage),几何阶段(Geometry Stage),光栅化阶段(Rasterizer Stage),像素处理阶段(Pixel Processing)。在《Real–Time Rendering 4th》中非常透彻的讲解了实时渲染的各种知识点,对渲染原理感兴趣的可以看看这本书,这本书堪称“实时渲染圣经”。下边会简单介绍一下这几个过程。
简而言之,就是图像在应用中的处理阶段。说白了就是一段运行在 CPU 上的程序,这时还没有 GPU 什么事。这一阶段主要是 CPU 负责处理用户的交互和操作,然后做一些应用层布局相关的处理,最后输出图元(点、线和三角形)信息给到下一阶段。
大家可能会疑惑 ,图元只有简单的点、线、三角形,能表示丰富的立体图形么,下边这张立体感很强的海豚就能给出肯定的答案了,简单的三角形再加上不同的着色,就能呈现出立体图形。
1. 顶点着色器(Vertex Shader)
顶点着色器可以对顶点的属性进行一些基本的处理。将顶点信息进行视角转换、添加光照信息、增加纹理等操作。CPU 丢给 GPU 的信息,就好像是站在上帝视角把这个视角看到的所有信息都给到 GPU。而 GPU 则是站在人类的角度,将人类可以观察到的画面,输出在显示器上。所以这里是以人的视角为中心,进行坐标转换。
2. 形状装配(Shape Assembly)这个阶段是将顶点着色器输出的所有顶点作为输入,并将所有的点装配成指定图元的形状。图元(Primitive
)如:点、线、三角形。这个阶段也叫图元装配。
3. 几何着色器(Geometry Shader)在图元外添加额外的顶点,将原始图元转换成新图元,来构建更加复杂的模型。
光栅化阶段会把前三个几何阶段处理后得到的图元(primitives)转换成一系列的像素。
如上图所示,我们可以看到,每个像素的中心有一个点,光栅化便是用这个中心点来进行划分的,如果中心点在图元内部,那么这个中心点所对应的像素就属于该图元。简而言之,这一阶段就是将连续的几何图形转化为了离散化的像素点。
1. 片段着色器(Fragment Shader)
通过上述的光栅化阶段之后,我们就拿到了各个图元对应的像素,最后这个阶段要做的事情就是给每个 Pixel 填充上正确的颜色,然后通过一系列处理计算,得到相应的图像信息,最终输出到显示器上。这里会做内插,就像补间动画一样。比如想要把一系列散点连成平滑曲线,相邻的已知点之间可能会缺少很多点,这时候就需要通过内插填补缺少的数据,最终平滑曲线上除已知点之外的所有点都是插值得到的。同样的,三角形的三个角色值给定后,其它的片段则根据插值计算出来,也就呈现来渐变的效果。
2. 测试与混合(Tests and Blending)
这个阶段会检测对应的深度值(z 坐标),来判断这个像素位于其它图层像素的前面还是后面,决定是否应该丢弃。此外,该阶段还会检查 alpha
值( alpha
值定义了一个像素的透明度),从而对图层进行混合。( 一句话简单说,就是检查图层深度和透明度,并进行图层混合。)
R = S + D * (1 - Sa)
含义:
R:Result,最终像素颜色。
S:Source,来源像素(上面的图层像素)。
D:Destination,目标像素(下面的图层像素)。
a:alpha,透明度。
结果 = S(上)的颜色 + D(下)的颜色 * (1 - S(上)的透明度)
经历了上边漫长的流水线之后我们便可以拿到屏幕绘制所需要的原始数据源-位图数据,然后由视频控制器把位图数据显示在物理屏幕上。
上边铺垫完渲染相关的一些基础知识之后,下面主要介绍 iOS 渲染相关的一些原理和知识。下图是 iOS 的图形渲染技术栈,有三个相关的核心系统框架:Core Graphics
、Core Animation
、Core Image
,这三个框架主要用来绘制可视化内容。他们都是通过 OpenGL 来调用 GPU 进行实际的渲染,然后生成最终位图数据存储到帧缓冲区,视频控制器再将帧缓冲区的数据显示物理屏幕上。
UIKit
UIKit 是 iOS 开发者最常用的框架,可以通过设置 UIKit 组件的布局以及相关属性来绘制界面。但是 UIKit 并不具备在屏幕成像的能力,这个框架主要负责对用户操作事件的响应(UIView 继承自 UIResponder),事件经过响应链传递。
Core Animation
Core Animation 主要负责组合屏幕上不同的可视内容,这些可视内容可被分解成独立的图层也就是我们日常开发过程中常接触的 CALayer,这些图层被存储在图层树中。CALayer 主要负责页面渲染,它是用户能在屏幕上看见的一切的基础。
Core Graphics
Core Graphics 主要用于运行时绘制图像。开发者可以使用此框架来处理基于路径的绘图,转换,颜色管理,离屏渲染,图案,渐变和阴影等等。
Core Image
Core Image 与 Core Graphics 正好相反,Core Graphics 是在运行时创建图像,而 Core Image 则是在运行前创建图像。
OpenGL ES 和 Metal
OpenGL ES 和 Metal 都是第三方标准,基于这些标准具体的内部实现是由对应的 GPU 厂商开发的。Metal 是苹果的一套第三方标准,由苹果实现。很多开发者都没有直接使用过 Metal,但却通过 Core Animation、Core Image 这些核心的系统框架在间接的使用 metal。
上边渲染框架中提到的 Core Animation 是 iOS 和 OS X 上图形渲染和动画的基础框架,主要用来给视图和应用程序的其他可视元素设置动画。Core Animation 的实现逻辑是将大部分实际绘图的工作交给 GPU 加速渲染,这样不会给 CPU 带来负担,还能实现流畅的动画。CoreAnimation 的核心类是 CALayer,UIKit 框架的核心类是 UIView,下边详细介绍一下这两个类的关系。
如上图所示,UIView 和 CALayer 是一一对应的关系,每一个 UIView 都有一个 CALayer 与之对应,一个负责布局、交互响应,一个负责页面渲染。
他们的两个核心关系如下:
CALayer 是 UIView 的属性之一,负责渲染和动画,提供可视内容的呈现。
UIView 提供了对 CALayer 功能的封装,负责了交互事件的处理。
举一个形象一点的例子,UIView 是画板,CALayer 就是画布,当你创建一个画板的时候,会自动绑定一个画布,画板会响应你的操作,比如你可以移动画板,画布则负责呈现具体的图形,二者职责分明。一个负责交互,一个负责渲染绘制。
为什么要分离出 CALayer 和 UIView?
iOS 平台和 MacOS 平台上用户的交互方式有着本质的不同,但是渲染逻辑是通用的,在 iOS 系统中我们使用的是 UIKit 和 UIView,而在 MacOS 系统中我们使用的是 AppKit 和 NSView,所以在这种情况下将展示部分的逻辑分离出来跨平台复用。
CALayer 中的 contents 属性保存了由设备渲染流水线渲染好的位图 bitmap(通常被称为 backing store),也就是我们最开始说的屏幕绘制需要的最原始的数据源。而当设备屏幕进行刷新时,会从 CALayer 中读取生成好的 bitmap,进而呈现到屏幕上。
@interface CALayer : NSObject
/** Layer content properties and methods. **/
/* An object providing the contents of the layer, typically a CGImageRef,
* but may be something else. (For example, NSImage objects are
* supported on Mac OS X 10.6 and later.) Default value is nil.
* Animatable. */
@property(nullable, strong) id contents;
@end
其实早在 WWDC 的 Advanced Graphics and Animations for iOS Apps(WWDC14 419,关于 UIKit 和 Core Animation 基础的 session)中苹果就给出了 CoreAnimation 框架的渲染流水线,具体流程如下图所示:
整个流水线中 app 本身并不负责渲染,渲染则是由一个独立的进程负责,即 Render Server 进程。下边会详细介绍一下整个 pipeline 的流程。
应用阶段
视图的创建
布局计算
对图层进行打包,在下一次 RunLoop 时将其发送至 Render Server
app 处理用户的点击操作,在这个过程中 app 可能需要更新视图树,如果视图树发生更新,图层树也会被更新
其次,app 通过 CPU 完成对显示内容的计算