iOS 底层渲染原理

OpenGL render theory on iOS

iOS 底层渲染原理

写在前面

下半年做过一次分享会,是以板书的形式分享。当时留下了一些手稿,最近整理一下分享给更多的人。

iOS 系统是如何实现 AddSubView 的?

在 iOS 开发中绝大部分的 UI 操作都靠 UIKit 完成,苹果已经让 UIKit 足够好用。

在你使用 UIKit 的 AddSubView 时,有没有想过 iOS 是如何更改屏幕上的对应像素的?苹果为什么不让开发者在辅助线程使用UIKit?

本文将从 AddSubView 出发,带你了解 iOS 底层是如何完成渲染的。

我用 OpenGL 来描述系统的渲染行为,虽然新的系统中 OpenGL 已经被 Metal 代替,但原理是类似的。

iOS 渲染框架

iOS 的渲染渲染框架有 UIKit,CoreAnimation/Quartz,OpenGL,Metal。

iOS 渲染框架

UIKit,CoreAnimation 可以看做是底层渲染技术的 shell 层,它们并不直接提交渲染命令给 GPU,而是做数据的组装和传递。此外 OpenGL 在 A11 以后已经不再直接调用系统驱动,而是调用 Metal 完成渲染。

UIKit 已经帮你做了数据的抽象,UI 工作可以很简单地完成。CoreAnimation 层能接触到更多的数据,如动画时间曲线,CAlayer 等。UIView 的操作单位是 Point,CALayer 的是 Pixels。

UIKit 不用关注 Pixels 的变化,而 Pixels 的操作流程正是整个渲染流程的关键,也是这次分享的主题。

硬件相关

为了更容易理解底层渲染技术,了解一些 GPU 的硬件知识是有必要的。

GPU-CPU硬件结构

GPU 与 CPU 的最大区别就是它是高度专用化的,它的目的就是更快地渲染。这个问题等效于更快计算出每个像素的值,所以 GPU 在硬件结构上做了对应的取舍。

GPU 大大弱化了逻辑控制的单元,把大部分芯片空间都让给了算术逻辑单元(ALU),Excution context 管理计算的上下文。这样 SIMD(单指令多数据)架构上保证了一批像素的同一个计算,只需要一次操作就可以完成,这也是数据并行加速的原理。

移动端 GPU

ARM 架构下,GPU 主要由 Imagination 和高通垄断,其中 A11 之后 iOS 上 GPU 由苹果自研。

CPU 和 GPU 之间靠总线传递数据,而移动端的总线带宽相比 PC 低很多,所以经常出现 GPU 空等 CPU 的情况。

虽然 GPU 自身的处理能力很强,但带宽限制了数据的吞吐量,所以压缩纹理和 TBR/TBDR 的应运而生。

bandwidth瓶颈

需要关注什么?

除了压缩纹理和 TBR/TBDR,各个厂家同样在驱动层为了同样的目的做了一些优化。对于 UI 操作需要关注的是:

  1. 渲染命令异步提交

你在 CPU 提交的渲染命令是异步提交给 GPU 的(前提是可以异步的命令,同步命令 CPU 要等 GPU 执行完)。具体流程是

CPU -> CPU command queue -> 系统调度 -> GPU command queue -> GPU

所以在更改 UI 的当前 RunLoop ,GPU 实际还没有开始绘制,不要预期有同步的渲染结果。

因为是异步提交命令,所以OpenGL Crash的现场并不是真正的问题所在,而是之前的命令导致状态机错误,引发了Crash。

  1. 多帧缓存

系统底层驱动一般至少会做两帧的缓冲,保证当前的渲染命令不影响当前上屏。但一些驱动会做更多的缓冲,iPhone8 以上至少会做 3 份缓冲,而这些缓冲是拿不到的,所以理论上从你的 UI 操作提交到 GPU,到真正展示会隔一小段时间。所以不要试图精准地拿到对应的 CALayer 数据。

  1. 每一帧清屏是个好习惯

现在大部分移动端GPU都是TBR/TBDR的,如果要保留上一帧渲染结果,会有额外的GPU内存读取操作,每一帧都清空画布会带来更高的性能,这也是苹果推荐用不透明视图的原因(部分原因)。苹果默认会这样设置CALayer,这也是开启多帧缓冲的先决条件。

render pipeline

渲染管线是一次 GPU 渲染数据的操作流程,这个过程中 OpenGL 完成了屏幕 坐标的确认和对应屏幕像素值得计算。

render pipeline

渲染管线可以分为两部分:

1. geometry pipeline

这个过程确定了输入的 3D 坐标在屏幕上对应的 2D 坐标。

对于 CPU 传来的 3D 顶点数据,会经过 Vertex processing 和 vertex post processing 两个过程,转换成 NDC 坐标(标准化设备坐标),经过图元组装后,就可以知道当前图形影响屏幕上哪些像素。

对于 AddSubView 来说,就是通过 subView 的 frame,确认了 subView 对应的像素。

下面讲一下 3D 坐标转 2D 坐标的过程,不感兴趣的可以跳过。

geometry pipeline

geometry pipeline 可以分为 3 个部分:

1. vertex processing

顶点输入后,要在 Vertex shader 中做一系列坐标变换,最终将 3D 坐标转换到一个[-1, 1]的空间内。

之后有可选的 Tessllation 和 Geometry shader 阶段,它们可以动态改变图元:比如改变图元的形状,增加图元等。(使用 Tesslation 或 Geometry shader 后,管线会提前一部分图元装配的工作,这样就保证了它们可以操作图元)。

这一部分在 OpenGL 中是可编程的,我们可以控制顶点数据如何变换。

2. vertex post processing

这一部分是不可编程的,渲染管线会自动调用。

经过前面的处理,顶点坐标被变换成各分量都在[-1,1]之间的裁剪坐标,不在此区间的坐标被丢弃。

接下来会做透视除法,将坐标转换为 NDC 坐标,你可以理解为完成了近大远小的缩放。

最后做 viewport 变换,将 NDC 坐标根据屏幕的像素大小(viewport 参数),映射成屏幕坐标。

变换管线

你可能注意到在 geometry pipeline 阶段我说到了多种不同的坐标,这其实正是 OpenGL 确定 3D 坐标到 2D 坐标的过程,实际上每一种坐标和相应的变换都是固定的。

transform pipeline

在第一部分,CPU 传给 GPU 的顶点坐标为局部坐标,所有位置都是相对于自身原点的。

之后通过 vertex shader 中的model transfrom, view transfrom, projection transfrom,变为裁剪坐标,这一部分是决定如何展示顶点的主要工作,。

然后通过 vertex post-processing 阶段,做透视除法和viewport transform,最终得到屏幕坐标。

最终输出的屏幕坐标,会作为下一步图元装配的输入。

这一部分可能比较难理解,你可以认为这一阶段是在 3D 世界中,如何用照相机照相。

图元装配

图元装配即完成用图元来描述你要绘制的形状。图元一般是三角形,因为在 3D 中,三角形能唯一确定一个平面。

对 AddSubView 来说,subView 会被组装成两个三角形图元。图元装配之后,就可以确定当前图形的屏幕区域。

2. pixels pipeline

pixels pipeline 主要完成了计算每个像素值的工作。

主要是下面三步:

格栅化

图元装配确定了图形的屏幕区域,格栅化是在硬件层面,确定当前图形会影响屏幕上哪些像素。所有包含在图形内的像素,作为下一阶段的输入。

fragment processing

格栅化之后,渲染管线会将这一批像素作为 fragment shader 的输入。这一过程是可编程的。

在 fragment shader 中,需要计算出每个像素是什么值。对 AddSubView 来说,如果 subView 的背景是图像,需要把图像作为纹理传到 GPU,在计算对应像素时去采样纹理。

也就是说这一阶段确定了 subView 的样子。

test & blend

最后这个决定了像素是否应该被展示。

stencil test

如果开启了 stencil test,那么像素对应的 stencil 值不为 0,像素才会被展示。

depth test

如果开启了 depth test,像素必须没有被遮挡(z 轴的前后关系),才会被展示。

blend

如果开启了 blend,像素的会根据 blend function 和 alpha 值,确定最终的像素值。

对 AddSubView 来说,如果 subView 设置了 mask,那么就要设置 stencil 来剔除 mask 之外的像素。如果 subView 是半透明,那么要根据 alpha 值和背景的颜色做 blend 操作。

UIKit 如何实现 addSubView

现在,再来说说 UIKit 如何实现 addSubView 的。

首先计算出 subView 的 frame,获取背景图像并解码成位图,获取 mask,alpha 等属性。

  1. 创建 GLContext。

  2. 取得当前 CALayer,作为当前 GLContext FBO 的 RBO。

  3. 将 frame 的四个顶点传给 vertex shader。将背景图像的位图传给 fragment shader。

  4. 在 vertex shader 中,由于是纯 2D 的界面,可以直接将顶点 z 轴坐标都设为 1。将坐标转为相对于 window 的坐标(或者使用正视投影)。

  5. 在 fragment shader 中,每个 SubView 的像素,都从图像中采样,作为输出的颜色值。

  6. 在最后的 test & blend 中,对 subView 的 alpha,mask 属性做对应处理。

  7. 展示当前 GLContext RBO。

这个流程也是系统 UIKit -> CoreAnimation -> render server 的过程。

OpenGL 状态机

你可以把 GLContext 理解为 OpenGL 的状态机,但实际上它是状态机的超集。

OpenGL 状态机

GLContext 可以看做是一个巨大的结构体,它维护着当前的渲染状态,包括 GLObject 也是一个状态的子集。

每一条渲染命令,其实都是在更改当前的渲染状态,从而最终影响渲染流程。
而苹果会自动在主线程设置一个 GLContext,来实现 UI 的渲染。

最后

每个 iOS 开发者都知道不能在辅助线程做 UI 操作,绝大多数却不知道为什么苹果要做这个限制。

看完了这篇文章,你应该能明白为什么:多线程操作 UI,意味着多线程在一个GLContext上渲染,苹果默认不会提交辅助线程的渲染命令到GPU(虽然苹果现在用的是Metal,并且渲染是单独一个进程,但原理类似,操作GPU的接口不是线程安全的)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • 转载自VR设计云课堂[//www.greatytc.com/u/c7ffdc4b379e]Unity S...
    水月凡阅读 1,007评论 0 0
  • 渲染管线 苹果提供了两种OpenGL ES的可视化模型,一种是客户端—服务端的架构模型,另一种就是管线的模型。 客...
    sellse阅读 12,116评论 1 10
  • 本文首发于个人博客:Lam's Blog - 【OpenGL ES】入门及绘制一个三角形,文章由MarkDown语...
    格子林ll阅读 7,248评论 2 18
  • 转载注明出处:点击打开链接 Shader(着色器)是一段能够针对3D对象进行操作、并被GPU所执行的程序。Shad...
    游戏开发小Y阅读 3,325评论 0 4
  • 1.对着镜子里的自己说我爱你,无论你是怎样的,无论你表现得好不好,无论你有多少缺点,我都要好好的爱你,突然很想哭,...
    妙知阅读 376评论 0 3