iOS 渲染原理总结
1. 图像渲染流程
如图所示,CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器按照垂直同步信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示
2. 什么是离屏渲染
正常的渲染流程如图所示,App 通过 CPU 与 GPU 合作,不停的将内容渲染完成,放入到 FrameBuffer 中,显示屏不断的从 FrameBuffer 中读取数据,显示到屏幕上
而离屏渲染则是先创建离屏渲染缓冲区 OffscreenBuffer,将渲染好的内容放入其中,等到合适的时机再将 OffscreenBuffer 中的内容进一步叠加、渲染,完成后才将内容放入 FrameBuffer中。
3. 为什么要减少离屏渲染
- 离屏渲染需要 App 对内容进行额外的渲染,并保存到 OffscreenBuffer ,
- 需要对 OffscreenBuffer 和 FrameBuffer 中的内容进行切换(Buffer 切换的代价比较大)。
- OffscreenBuffer 本身需要额外的空间,大量的离屏渲染可能早晨过大的内存压力。
4. 离屏渲染的具体过程是怎样的
图层的叠加绘制,大体遵循“画家算法”。如图所示,在这种算法下会按层绘制,先绘制距离较远的场景,然后绘制距离较近的场景,覆盖较远的部分。因此在普通的layer绘制中,上层的 subLayer 会覆盖下层 subLayer,下层 subLayer 绘制完成后就可以抛弃了,从而节约空间。所有的 subLayer 绘制完成后,整个绘制就完成了,就放入 frameBuffer 准备呈现到显示器上。
而当设置了 cornerRadius 和 maskToBounds = true 时,maskToBounds 会应用到所有的 subLayer 上。这也意味着所有的 subLayer 必须要重新应用一次圆角+裁切,因此所有的 subLayer 在第一次绘制结束后不能被丢弃,而必须保存在 OffscreenBuffer 中等待下一轮圆角+裁切,因此诱发了离屏渲染。
5. iOS 渲染具体过程
app 处理流程如下
app 本身不负责渲染,渲染则是由一个独立的进程负责,即 Render Server 进程。App 将渲染任务和数据提交给 Render Server,Render Server 处理完数据后再传递给 GPU,最后由 GPU 调用 iOS 的图像设备进行显示。具体流程如下:
1. app 处理事件如用户的点击,在此过程中 app 可能需要更新时图树,相应的涂层树也会更新。
2. app 通过 CPU 完成对显示内容的计算,如视图的创建、布局计算、图片解码、文本绘制等。在完成对显示内容的计算后,app 对图层进行打包,并将其发送至 Render Server,即完成了一次 Commit Transaction 操作。
3. Render Server 执行 OpenGL、CoreGraphics 相关程序,并调用 GPU
4. GPU 则在物理层上完成对图像的渲染。
5. GPU 通过 frameBuffer 、视频控制器等相关部件,将图像显示在屏幕上。
上述步骤远超过了 16.67ms,因此为了满足对屏幕 60FPS 的刷新率的支持,需要将这些步骤分解,以流水线的形式,并行执行,如下图所示。
其中,app 调用 Render Server 前的最后一步 Commit Transaction 可以分为4个步骤:Layout、Display、Prepare、Commit
-
Layout 阶段:进行视图构建,包括
LayoutSubviews
方法重载、addSubView
方法填充子视图等 -
Display 阶段:主要进行视图绘制,这里的绘制是指设置最终要成像的图元数据。重载视图的
drawRect:
方法,可以自定义UIView
的显示,其原理是在drawRect:
方法内部绘制寄宿图,该过程使用 CPU 和内存 -
Prepare 阶段:属于附加步骤,一般处理图片的解码与转换操作
-
Commit 阶段:主要将图层打包,发送到 Render Server。该过程会递归进行,因为图层是以树形结构存在的
GPU 渲染流程如下
上图是一个三角形被渲染的过程中,GPU 所负责的渲染流水线。
GPU 获取到的图像信息称为图元,通常是三角形、顶点、线段等。主要分为以下几个阶段- Geometry 几何处理阶段:包含顶点着色器、形状装配、集合着色器
- 顶点着色器:这个阶段会将图元中的顶点信息进行视角转换、添加光照、增加纹理等操作
- 形状(图元)装配:根据上一步中的顶点位置,装配成基本的图元形状,例如三角形
- 集合着色器:增加额外的顶点,将原始图元转化成新的图元,以构建一个不一样的模型。总的来说就是基于三角形、线段、点构建更复杂的几何图形
-
Rasterization 光栅化阶段:图元转换为像素
光栅化的目的是将集合渲染后的图元信息转换为一系列的像素,同时会裁掉屏幕显示范围之外的内容,以便后续显示在屏幕上。这个阶段会根据图元信息,计算出每个图元所覆盖的像素信息,从而将像素划分成不同的部分。 一种简单的方法是根据像素的中心点。如果一个像素的中心点在图元的内部,那这个像素就属于这个图元。如图所示,深蓝色的线是图元信息构建出的三角形,而根据是否覆盖中心点就可以遍历出属于该图元的所有像素,即淡蓝色的部分。
- Pixel 像素处理阶段:处理像素,得到位图(bitmap)
经过了光栅话阶段,得到了图元对应的像素,此时需要给这些像素填充颜色和效果。所以最后这个阶段就是给像素填充正确的内容,最终显示在屏幕上。这些经过处理、包含大量信息的像素点称为位图(bigmap)。即 Pixel 阶段输出的就是位图。具体如下
- 片段着色器(Fragment Shader):这个阶段的目的是给每一个像素赋予正确的颜色,颜色的来源就是之前得到的顶点、纹理、光照之类的3D数据等信息,由于需要处理纹理、光照等复杂信息,这通常是整个系统的性能瓶颈。
- 测试与混合(Tests and Blending或Merging)阶段:因为每个屏幕像素点上可能堆叠了多个颜色数据,所以要根据深度值 z 坐标、透明度 alpha 值,从而进行片段的混合,得到最终的颜色。
- Geometry 几何处理阶段:包含顶点着色器、形状装配、集合着色器
6. GPU 渲染流程反映到具体的 iOS 代码上都有哪些步骤
- 设置 layer 的类型为
CAEAGLLayer
- 新建
EAGLContext
类型的上下文,并设置为当前的上下文 - 清除掉之前的旧的 renderBuffer 和 frameBuffer
- 创建新的 renderBuffer 和 frameBuffer
- 分配
renderBuffer
的内存空间,并绑定到 layer 上 - 构造顶点着色器并编译
- 构造片段着色器并编译
- 构造着色器程序,并关联顶点、片段着色器
- 创建顶点对象,链接顶点属性
- 设置背景色
- 清空颜色缓冲数据
- 设置渲染窗口
- 激活着色器程序
- 关联数据
- 最终绘制
7. 为何要图片解码,直接显示图片有什么问题?
图片格式
- 位图:位图是一个像素数组,数组中的每个像素就代表着图片中的一个点
- JPEG、PNG: 一种压缩的位图图形格式。其中 PNG 图片是无损压缩,并且支持 alpha 通道,JPED 图片是有损压缩,可以指定 0~100% 的压缩比
解码
通过网络下载的图片或者本地的图片,都是 JPEG、PNG这些格式的压缩图片。在将这些图片渲染到屏幕之前,首先要得到图片的原始像素数据,才能执行后续的绘制操作
iOS 默认在主线程对图像进行解码,解压缩后的图片大小与原始文件大小无关,只与图片像素有关
解压缩后的图片大小 = 图片的像素宽 * 图片的像素高 * 每个像素所占的字节数
8. render server 具体是啥,负责什么工作,为何需要单独的 RenderServer 进程
iOS Render Server 是 OpenGL ES & Core Graphics。Render Server 将与 GPU 通信把数据经过处理之后传递给 GPU。主要为
- 负责解析图层树,反序列化为渲染树:使用这个树状结构,渲染服务队动画的每一帧做出如下工作:对所有的图层属性计算中间值,设置 OpenGL 几何形状(纹理化的三角形)来执行渲染。
- 调用绘制指令,并提交到 GPU
这两个阶段在动画过程中不停的重复。阶段 1 在软件层面通过CPU 处理,阶段 2 被 GPU 执行。
图层树
屏幕上的可视内容倍分解成为独立的图层(CALayer),这些图层被存储在一个叫做图层树的体系中。
App 并不是完全占有 Screen,还有状态栏、通知栏、上拉菜单等,需要一个统一的 Server 来负责
9. layer 为啥可以显示内容?layer 的backing store 与 frameBuffer(帧缓存或显存、显示存储器) 都是啥,里面存的都是位图?关系如何?
CALayer 包含一个 contents
属性指向一块缓存区,称为 backing store
,可以存放位图。iOS 中将该缓存区保存的图片称为寄宿图(CGImageRef
)
GPU 包含着一部分显存空间(VRAM),在设备启动的时候,作为 PCI 设备的 GPU,其显存空间中的一部分地址,会被映射到 PCI 地址中,然后再把 PCI 总线上的地址映射到 CPU 地址中。这样 CPU 就能通过对这段映射后的地址的访问,访问到 GPU 的存储空间。此外还可以通过内存空间进行数据交互。首先系统为 GPU 动态分配一些不连续的空间(GTT),用于映射到 GPU 显存空间中。然后 CPU 通过对这段空间的访问,进行对 GPU 显存空间的访问。对于 GPU 指令来说,则是直接由 CPU 通过 PCI 总线推送到 GPU 中,或是由 GPU 自己从指令流中获取指令。
layer 的backing store
和 frameBuffer 存储的都是位图。frameBuffer 中的位图是最终显示到屏幕上的。
以显示图片为例。图片数据是以纹理形式,进入 OpenGL 渲染流程的,就像上面的流程图。
参照问题5(iOS 渲染具体过程)layer 中的位图数据会与其他的位图(如果还有其他 layer的话)经过片段着色器、测试与混合阶段,变成新的位图数据,输出到 frameBuffer 显示到屏幕上
思考
- 想显示渐变带边框带阴影的 View 有几种方式?各有什么优缺点?
- 在 TableViewCell 中显示圆形头像有几种方式?各有什么优缺点?
- maskToBounds 与 cornerRadius 组合:离屏渲染
- 上方叠加一个中间圆形镂空的图片:多了一层 ImageView
- UIBezierPath 设置的 CAShapeLayer 作为 UIImageView.layer.mask:离屏渲染
- CoreGraphics 设置处理 UIImage ,注意是直接将 UIImage 绘制成圆形,而不是调用 UIImageView
drawRect:
:推荐
func ny_image(byRoundCornerRadius radius: CGFloat,
corners: UIRectCorner,
borderWidth: CGFloat,
borderColor: UIColor,
borderLineJoin: CGLineJoin) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(size, false, scale)
let context = UIGraphicsGetCurrentContext()
let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
// 根据坐标系旋转图片
context?.scaleBy(x: 1, y: -1)
context?.translateBy(x: 0, y: -rect.size.height)
// 剪成圆形
let minSize = min(size.width, size.height)
if borderWidth < minSize / 2 {
let path = UIBezierPath.init(roundedRect: rect.insetBy(dx: borderWidth, dy: borderWidth), byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
path.close()
context?.saveGState()
path.addClip()
context?.draw(cgImage!, in: rect)
context?.restoreGState()
}
// 画边框
if (borderColor != .clear) && (borderWidth < minSize / 2) && (borderWidth > 0) {
let strokeInset = (floor(borderWidth * scale) + 0.5) / scale
let strokeRect = rect.insetBy(dx: strokeInset, dy: strokeInset)
let strokeRadius = radius > scale / 2 ? radius - scale / 2 : 0
let path = UIBezierPath.init(roundedRect: strokeRect, byRoundingCorners: corners, cornerRadii: CGSize(width: strokeRadius, height: strokeRadius))
path.close()
path.lineWidth = borderWidth
path.lineJoinStyle = borderLineJoin
borderColor.setStroke()
path.stroke()
}
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}