一、CPU 和 GPU
CPU 是计算机整个系统的运算核⼼、控制核⼼。CPU 内部采用的是流水线结构, 使其拥有一定程度的并行计算能力。CPU 擅长分支预测等复杂操作。
GPU 是可进行绘图运算工作的专用微处理器,是一个被高度优化设计的用来并发计算的硬件单元。GPU擅长对大量数据进行大量并行的工作,GPU 各个单元依懒性非常低。
1.1 CPU 和 GPU异构系统
在实际应用中 CPU 和 GPU 有两种异构系统来协同工作
- 左边是分离式架构系统,CPU 和 GPU 拥有各自存储系统。CPU 的内存也是我们所说的手机或是电脑内存,GPU 的内存是显存(帧缓冲)也就是图形顶点数据存储的位置。这种结构的缺点在于 PCI-e 相对于两者具有低带宽和高延迟,数据的传输成了其中的性能瓶颈。目前使用非常广泛,如PC、智能手机等。
- 右边是耦合式架构系统,CPU 和 GPU 共享内存和缓存。主要应用在游戏主机中,如 PS4。
1.2 CPU 和 GPU工作流程
当 CPU 遇到图像处理的需求时,会调用 GPU 进行处理,主要流程是这样:
- 将主存的处理数据复制到显存中
- CPU 指令驱动 GPU
- GPU 中的每个运算单元并行处理
- GPU 将显存结果传回主存
1.3 GPU 图形渲染流程
GPU 图形渲染流程具体可以分为六个步骤:
- 顶点着色器
- 图元装配
- 几何着色器
- 光栅化
- 片元着色器
- 测试与混合
开发人员可以使用 GLSL 语言进行对顶点着色器和片元着色器修改,这也是唯一能对图形修改的地方,片元着色器主要是对图形的饱和度进行修改。
二、图像显示方式和原理
随机扫描显示
在随机扫描方式中,电子束能在屏幕上进行随机移动,轨迹随显示内容变化而变化,只在需要显示字符和图形的地方扫描,而不是全屏扫描。一般用于高清晰度的专用图形显示器中。特点:
- 显示速度快、画面清晰、线条轮廓十分光滑
- 控制方式复杂,只能用于字符和图形显示,不适用于显示随机图像
光栅扫描显示
在光栅扫描方式中,电子束在水平和垂直同步信号的控制下有规律的扫描整个屏幕。特点:
- 控制方式简单
- 画面质量较好和稳定
- 对扫描频率要求高
那为什么人眼感觉不到屏幕在刷新呢?这里有个专有名词"视觉暂留",人眼在观察景物的时候,光信号传入大脑神经,需经过一段短暂的时间,光的作用结束后,视觉形象并不会立即消失。在人眼中1秒16帧的画面就是连续的了。
图像显示流程
CPU 计算好显示内容提交至 GPU,GPU 渲染完成后将渲染结果存入帧缓冲区,视频控制器会按照 VSync 信号逐帧读取帧缓冲区的数据(位图),经过数模转换(数字信号->模拟信号)后最终由显示器(逐行扫描)进行显示.
帧缓冲和垂直同步
为了效率问题,GPU 通常会引入两个缓冲区,即双缓冲机制。在这种情况下,GPU 会预先渲染一帧放入一个缓冲区中,用于视频控制器的读取。当下一帧渲染完毕后,GPU 会直接把视频控制器的指针指向第二个缓冲区。
双缓冲虽然能解决效率问题,但会引入一个新的问题。造成画面撕裂现象,原因是视频控制器还未读取完 A 帧缓冲区数据时,显示到屏幕一半的画面,当GPU 将新的一帧数据提交到 B 帧缓冲区并且交换缓冲区后,视频控制器就会把新的一帧数据的下半段显示到屏幕上,就会出现画面撕裂的现象。
为了解决这个问题,GPU 通常有一个机制叫做垂直同步,垂直同步技术相当于在帧缓冲区加锁操作,当电子束都扫描完成以后才去读取数据。当开启垂直同步后,GPU 会等待显示器的 VSync 信号发出后,才进行新的一帧渲染和缓冲区更新。这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟。
iOS 设备会始终使用垂直同步+双缓存区的策略。
掉帧
垂直同步+双缓存区解决了画面撕裂问题,但是有出现了一个新的问题——掉帧。掉帧是重复渲染同一帧数据。屏幕显示 A 缓冲区的数据,当垂直同步信号到来时,B 缓冲区的数据还没有准备好,屏幕继续显示 A 缓存区的数据,等到 B 缓冲区数据准备完成后,交换缓冲区,按照这个方式一直执行下去,就会造成卡顿。
大致的流程是这样:
接收垂直同步信号 -> CPU 和 GPU 还没有准备好数据 -> 视频控制器就拿不到 FrameBuffer 里面的数据 -> 重复渲染同一帧数据
为了解决这个问题,提出了三缓存区机制,合理使用 CPU 和 GPU 的闲置时间,三缓存区也会存在掉帧问题,但是它能减少掉帧次数。这样的话卡顿的原因也就找到了:
- CPU 和 GPU 渲染流水线耗时过长——掉帧
- 垂直同步+双缓冲区是以掉帧为代价解决屏幕撕裂问题
三、iOS系统图像渲染流程
图形图形渲染框架介绍
- UIKit 主要提供界面呈现能力、事件响应能力、驱动 RunLoop 运行与系统内核通信。简单来说就是主要负责界面展示、事件响应以及是 RunLoop 的需求方。
- CoreAnimation 来自于 QuartzCore 框架,提供了图形处理和视频图像处理的能力。不要认为 CoreAnimation 只是负责核心动画,还负责通过调用 OpenGL ES 把我们用代码构建的界面显示到屏幕上。
- CoreGraphics 提供了非常强大的轻量级2D渲染能力。CoreGraphics 负责创建显示到屏幕上的数据模型。
三者的关系是通过界面展示以及动画的创建、执行关联起来的,所以它们之间是协作而不是从属的关系。
UIView 与 CALayer(CoreAnimation Layer) 关系
1. UIView 可以响应事件,CALayer 不可以
查看 UIKit 框架可以得知 UIView 是继承 UIResponder,这也就说明 UIView 有接收事件和响应事件处理的能力。CALayer 是继承 NSObject 所以说 CALayer 没有接收和响应事件处理的能力。
2. UIView 的属性与 CALayer 的属性是映射关系
一个 CALayer 的 frame 是由 anchorPoint、position、bounds、transform 共同决定的,对于一个 UIView 的 frame 只是简单返回 CALayer 的 frame 而已。同样的 UIView 的 center、bounds 也是返回 CALayer 的一些属性。
3. UIView 主要负责管理 CALayer ,CALayer 负责绘制
重写 [view drawRect:rect] 和 [layer display] 可以看出 UIView 是 CALayer 的代理,从而绘制出视图内容。
4. 执行动画的时候,是 CALayer 在为 UIView 做隐式动画
CALayer 修改属性是默认支持隐式动画的,在给 UIView 的 Layer 做动画的时候,UIView 作为 CALayer 的代理,CALayer 通过 actionForLayer:forKey: 向 UIView 请求相应的 action(动画行为)。
CALayer 内部维护着三种图层树,分别是 presentLayer Tree(动画树),modeLayer Tree(模型树), Render Tree(渲染树),在做动画的时候,我们其实修改的是 presentLayer Tree ,展示在界面上是 modelLayer Tree。
渲染流程
App 本身并不负责渲染任务,负责渲染任务是一个独立的进程—— Render Server。App 通过 IPC (进程间通信) 将渲染任务和相关数据提交给 Render Server。Render Server 处理完数据提交给 GPU。最后 GPU 渲染完成后把这一帧画面放到 FrameBuffer,等到 Vsync 信号到来,视频控制器就会把这一帧画面显示到屏幕上。
CoreAnimation 在 RunLoop 中注册了一个 Observer,监听 RunLoop 即将休眠(BeforeWaiting)和退出(Exit)事件。如果调整 UIView / CALayer 视图层级、属性设置,或是手动调用 UIView / CALayer 的 setNeedsLayout / setNeedsDisplay 方法后,这些操作都会被 CALayer 标记,并通过 CATransaction 提交到一个全局容器(Backing Store)去。当 RunLoop 即将休眠或是退出时,关注该事件的 Observer 就会在回调中把所有待处理的 UIView / CALayer 执行实际的绘制和调整,最终交给 GPU 进行渲染。如果图层有动画,通过 CADisplayLink 稳定的刷新机制会不断唤醒 RunLoop,使得不断触发 Observer 回调,从而根据动画时间来更新属性值并绘制出来。
在方法调用栈中可以很清晰的看出来每一个方法是做什么的,其中还包括了 UIView 与 CALayer 的关系。