上一篇文章只是大致翻译了一下419,如果仅仅是听他讲肯定还有很多我们不懂的地方,所以我需要用自己的语言重新去组织一下这个过程。
首先我们引出一个ios中的api叫做CADisplayLink,CADisplayLink是一个能让我们以和屏幕刷新率相同的频率将内容画到屏幕上的定时器。我们在应用中创建一个新的CADisplayLink对象,把它添加到一个runloop中,并给它提供一个target和selector在屏幕刷新的时候调用。
注:通常来讲:iOS设备的刷新频率是60HZ也就是每秒60次。那么每一次刷新的时间就是1/60秒,大概16.7毫秒。
为什么会有这个api,因为屏幕成像的原理是,CRT 的电子枪按照上面方式,从上到下一行行扫描,扫描完成后显示器就呈现一帧画面,随后电子枪回到初始位置继续下一次扫描。为了把显示器的显示过程和系统的视频控制器进行同步,显示器(或者其他硬件)会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行,准备进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称 HSync;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization),简称 VSync。显示器通常以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。(具体过程可以看下面的连接,这里只做简单介绍)
知道了一些基本原理我们再来看看419
注:这里重新讲述一下Core Animation的概念,Core Animation其实是一个令人误解的命名。你可能认为它只是用来做动画的,但实际上它是从一个叫做Layer Kit这么一个不怎么和动画有关的名字演变而来,所以做动画这只是Core Animation特性的冰山一角。Core Animation是一个复合引擎,它的职责就是尽可能快地组合屏幕上不同的可视内容,这个内容是被分解成独立的图层,存储在一个叫做图层树的体系之中。于是这个树形成了UIKit以及在iOS应用程序当中你所能在屏幕上看见的一切的基础。实际上 Core Animation 框架做了很多基础工作:组合屏幕上的内容,追踪视图结构和内容的变化。
经过我们前面的科普之后,想必在座的同学就能对这个图片有个进阶的理解了,在每个Vsync的周期内,都可以接收一些事件进行处理。
首先大致讲一下左边箭头向下的过程,这些都是app内部发生的。
1.事件处理(比如触摸点击,以致需要引发界面修改),这发生在(commit transaction) 阶段,在这步过程的最后会将view hierarchy编码好并发送给render server。
2.render server这时候需要解码这个视图层次结构。render server必须等到下一次的显示缓存空出之后才可以发起GPU绘制操作,然后,开始调用绘制的操作API (openGL 或者metal API)。
3.在上述绘制资源准备好之后,GPU开始进行渲染,这个渲染过程最好在在下一次同步时间结束之前完成(16.67ms的刷新时间,以确保60帧的帧频)以便将frame buffer中的画面呈现给用户,并将缓存交给下次绘制。
注:流程图中 Commit Transaction 前面的红框代表触发视图内容变化的事件,比如点击按钮,之后Core Animation 框架会捕获到屏幕内容的变化并提交给 Render Server(渲染服务器),Render Server 里另外一个版本的 Core Animation 框架负责解码并绘制内容。
我们先从commit transaction开始,这是事物提交阶段。
在事务提交阶段有四个过程
1.layout: 建立(set up)views,会调用重载的layoutSubviews方法,这里会发生view的创建,以及通过addSubview将layers添加进view层级中,将内容聚集起来,并做一些轻量的数据库查找(因为不能在这里停留太久,轻量级的操作可以是本地化字符串的查找以供应label的layout。
2.display: 绘制views,这个阶段是如果drawRect有重载的话会通过drawRect绘制内容或者做字符串绘制,需要注意的是这个阶段实际上是CPU或者内存密集型的,这里我们用core graphics里的CGContext来渲染,能减少工作量和性能消耗。
3.prepare commit:做一些例如图像解码和图像转换的工作,图像解码很容易理解,如果有JPEG/PNG格式的图片,我们则需要对他们进行解码,如果有其他格式的图片,你可能要进行一些图片转换,一个好的解决方案是你可以把它们转换成bitmap来处理。
4.commit:打包layers并提交给render server,这个过程是递归的,所以需要确保view树的平整以确保高效。
再来看看动画的工作流程,动画有三个过程,前两个发生在应用内,第三个在render server。
1.是创建动画更新视图利用动画的方法。
2.跟上面的事物提交阶段类似,不同的是不仅仅提交视图层次结构,还提交动画。这样我们就能持续update你的动画而不用回去跟应用建立连接。
3.讲讲渲染之前,先科普一下渲染的概念。
渲染概念
这一节介绍了一些基本的渲染知识:屏幕被分割成 NxN 像素的小块来渲染,每个小块的大小与 SoC(System on Chip) 的cache相关。具体的操作过程如下:对于一个 app icon,被当做一个 CALayer 来渲染,而 CALayer 在 Core Animation 中被划分为两个三角形,每个三角形可以被继续分割成多个三角形,对每一个三角形单独渲染。这样做的目的是为了在每个时间点都可以获取每个tile中各像素集齐之后色彩总体的几何信息,并决定哪些像素可见,并决定运行哪个pixel shader,确保每像素只运行每个pixel shader一次。但如果需要做blending的话,就不止一次了,那么会有overdraw的问题。
注:图中的网格称之为tile bucket,块渲染 Tile Based Rendering。
块渲染是目前移动GPU的主流渲染方式,因为这种方式更好地适配了移动设备的耗电和性能的平衡问题。究其原因,是因为GPU在运算时对数据带宽消耗的极高要求:OPENGL的虚拟管线需要大量的显存带宽来支持, 为了减少这个凶残的带宽需求,大多数移动GPU都使用了tiled-based渲染。在最基础的层面,这些GPU将帧缓存(framebuffer),包括深度缓存,多采样缓存等等,从主内存移到了一块超高速的on-chip存储器上,计算芯片就能以远低于常规消耗的电能来读写存储器。但是on-chip的存储器都不可能很大,否则GPU芯片的大小将大的吓人,在有些GPU中小到只能容纳16x16个像素,于是将OPENGL的帧缓存切割成16x16的小块(这就是tile-based渲染的命名由来),然后一次就渲染一块。对于每一块tile: 将有用的几何体提交进去,当渲染完成时,将tile的数据拷贝回主内存。这样,带宽的消耗就只来自于写回主内存了,那是一个较小的消耗,消耗极高的深度/模板测试和颜色混合完全的在计算芯片上就完成了。
有关Tile Based Rendering的更多内容,有兴趣可以简单翻一下这篇译文《Performance Tunning for Tile-Based Architecture》
注:
着色器是可编程管线中的术语,其语法类似C语言,分为顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)。
Vertex shader – 在你的场景中,每个顶点都需要调用的程序,称为“顶点着色器”。假如你在渲染一个简单的场景:一个长方形,每个角只有一个顶点。于是vertex shader 会被调用四次。它负责执行:诸如灯光、几何变换等等的计算。得出最终的顶点位置后,为下面的片段着色器提供必须的数据。
Fragment shader – 在你的场景中,大概每个像素都会调用的程序,称为“片段着色器”。在一个简单的场景,也是刚刚说到的长方形。这个长方形所覆盖到的每一个像素,都会调用一次fragment shader。片段着色器的责任是计算灯光,以及更重要的是计算出每个像素的最终颜色。
渲染过程
这块详见参考资料7