我们知道屏幕上的每个像素均有三个色素组成RGB(红绿蓝,还有透明度alpha),三个独立的颜色单元会根据给定的颜色显示到一个像素上。iPhone5 的屏幕有1,136×640=727,040个像素,因此有2,181,120个颜色单元。所有的图形堆栈一起工作以确保每次正确显示。当我们在活动屏幕时,数以百万计的颜色单元必须以每秒60次的速度刷新,这是一个很大的计算量。
图像的显示可以简单的用下图来表示;
Display 的上一层是图形处理单元GPU,GPU是一个专门为图形处理高并发计算而设计的处理单元,它能同时跟新所有的像素,并呈现在显示器上。它的高并发性能让它能高效的将不同纹理合成起来。GPU是非常专业的,处理图形速度非常快,比CPU使用更少的电来完成工作。通常的CPU可以做很多不同的事情,但是在合成图像上CPU却显得比较慢。
GPU Driver是直接和GPU交流的代码块。不同的GPU是不同的,但是驱动使它们在下一个层级上显得更为统一,典型的下一层级有OpenGL/OpenGL ES。OpenGL 是一个提供了 2D 和 3D 图形渲染的 API。GPU 是一块非常特殊的硬件,OpenGL 和 GPU 密切的工作以提高GPU的能力,并实现硬件加速渲染。对大多数人来说,OpenGL 看起来非常底层,但是当它在1992年第一次发布的时候(20多年前的事了)是第一个和图形硬件(GPU)交流的标准化方式,这是一个重大的飞跃,程序员不再需要为每个GPU重写他们的应用了。
OpenGL之上扩展出很多东西。在iOS上,几乎所有的东西都是通过Core Animation绘制出来的,然而在OS X上,绕过Core Animation直接使用Core Graphics绘制的清醒并不少见。对于一些专门的应用,尤其是游戏,程序可能直接和OpenGL/OpenGL ES交互。 Core Animation 使用 Core Graphics 来做一些渲染。像 AVFoundation,Core Image 框架也使用Core Graphics来渲染。
总之,GPU 是一个非常强大的图形硬件,并且在显示像素方面起着核心作用。它连接到 CPU。从硬件上讲两者之间存在某种类型的总线,并且有像 OpenGL,Core Animation 和 Core Graphics 这样的框架来在 GPU 和 CPU 之间精心安排数据的传输。为了将像素显示到屏幕上,一些处理将在 CPU 上进行。然后数据将会传送到 GPU,这也需要做一些相应的操作,最终像素显示到屏幕上。
正如上面图片显示:GPU 需要将每一个 frame 的纹理(位图)合成在一起(一秒60次)。每一个纹理会占用 VRAM(video RAM),所以需要给 GPU 同时保持纹理的数量做一个限制。GPU 在合成方面非常高效,但是某些合成任务却比其他更复杂,并且 GPU在 16.7ms(1/60s)内能做的工作也是有限的。
下一步就是将数据传输到GPU上,为了让 GPU 访问数据,需要将数据从 RAM 移动到 VRAM 上。这就是提及到的上传数据到 GPU。这看起来貌似微不足道,但是一些大型的纹理却会非常耗时。
最终,CPU 开始运行你的程序。你可能会让 CPU 从 bundle 加载一张 PNG 的图片并且解压它。这所有的事情都在 CPU 上进行。然后当你需要显示解压缩后的图片时,它需要以某种方式上传到 GPU。一些看似平凡的,比如显示文本,对 CPU 来说却是一件非常复杂的事情,这会促使 Core Text 和 Core Graphics 框架更紧密的集成来根据文本生成一个位图。一旦准备好,它将会被作为一个纹理上传到 GPU 并准备显示出来。当你滚动或者在屏幕上移动文本时,不管怎么样,同样的纹理能够被复用,CPU 只需简单的告诉 GPU 新的位置就行了,所以 GPU 就可以重用存在的纹理了。CPU 并不需要重新渲染文本,并且位图也不需要重新上传到 GPU。
单个位图显示流程整体如上所述,当我们在实际中使用的更多的是不同位图组合成的复杂图像,这就需要将不同的位图进行合成。在图形世界中,合成是一个描述不同位图如何放到一起来创建你最终在屏幕上看到图像的过程。这一过程在许多方面显得显而易见,却让人忘了背后错综复杂的计算。
让我们忽略一些难懂的事例并且假定屏幕上一切事物皆纹理。一个纹理就是一个包含 RGBA 值的长方形,比如,每一个像素里面都包含红、绿、蓝和透明度的值。在 Core Animation 世界中这就相当于一个 CALayer。
在这个简化的设置中,每一个 layer 是一个纹理,所有的纹理都以某种方式堆叠在彼此的顶部。对于屏幕上的每一个像素,GPU 需要算出怎么混合这些纹理来得到像素 RGB 的值,这就是合成。
如果我们所拥有的是一个和屏幕大小一样并且和屏幕像素对齐的单一纹理,那么屏幕上每一个像素相当于纹理中的一个像素,纹理的最后一个像素也就是屏幕的最后一个像素。如果我们有第二个纹理放在第一个纹理之上,然后GPU将会把第二个纹理合成到第一个纹理中。有很多种不同的合成方法,但是如果我们假定两个纹理的像素对齐,并且使用正常的混合模式,我们便可以用下面这个公式来计算每一个像素:R = S + D * ( 1 – Sa )
结果的颜色是源色彩(顶端纹理)+目标颜色(低一层的纹理)*(1-源颜色的透明度)。在这个公式中所有的颜色都假定已经预先乘以了他们的透明度。让我们进行第二个假定,两个纹理都完全不透明,比如 alpha=1.如果目标纹理(低一层的纹理)是蓝色(RGB=0,0,1),并且源纹理(顶层的纹理)颜色是红色(RGB=1,0,0),因为 Sa 为1,所以结果为R = S。结果是源颜色的红色。这正是我们所期待的(红色覆盖了蓝色)。如果源颜色层为50%的透明,比如 alpha=0.5,既然 alpha 组成部分需要预先乘进 RGB 的值中,那么 S 的 RGB 值为(0.5, 0, 0),公式看起来便会像这样:
R = S + D * (1 - Sa) =( 0.5,0,0) + (0,0,0) * (1 - 0.5) = (0.5,0,0.5)
我们最终得到RGB值为(0.5, 0, 0.5),是一个紫色。这正是我们所期望将透明红色合成到蓝色背景上所得到的。
记住我们刚刚只是将纹理中的一个像素合成到另一个纹理的像素上。当两个纹理覆盖在一起的时候,GPU需要为所有像素做这种操作。正如你所知道的一样,许多程序都有很多层,因此所有的纹理都需要合成到一起。尽管GPU是一块高度优化的硬件来做这种事情,但这还是会让它非常忙碌。
当源纹理是完全不透明的时候,目标像素就等于源纹理。这可以省下 GPU 很大的工作量,这样只需简单的拷贝源纹理而不需要合成所有的像素值。但是没有方法能告诉 GPU 纹理上的像素是透明还是不透明的。只有当你作为一名开发者知道你放什么到 CALayer 上了。这也是为什么 CALayer 有一个叫做 opaque 的属性了。如果这个属性为 YES,GPU 将不会做任何合成,而是简单从这个层拷贝,不需要考虑它下方的任何东西(因为都被它遮挡住了)。这节省了 GPU 相当大的工作量。这也正是 Instruments 中 color blended layers 选项中所涉及的。(这在模拟器中的Debug菜单中也可用).它允许你看到哪一个 layers(纹理) 被标注为透明的,比如 GPU 正在为哪一个 layers 做合成。合成不透明的 layers 因为需要更少的数学计算而更廉价。
所以如果你知道你的 layer 是不透明的,最好确定设置它的 opaque 为 YES。如果你加载一个没有 alpha 通道的图片,并且将它显示在 UIImageView 上,这将会自动发生。但是要记住如果一个图片没有 alpha 通道和一个图片每个地方的 alpha 都是100%,这将会产生很大的不同。在后一种情况下,Core Animation 需要假定是否存在像素的 alpha 值不为100%。在 Finder 中,你可以使用 Get Info 并且检查 More Info 部分。它将告诉你这张图片是否拥有 alpha 通道。
到现在我们都在考虑像素完美重合在一起的 layers。当所有的像素是对齐的时候我们得到相对简单的计算公式。每当 GPU 需要计算出屏幕上一个像素是什么颜色的时候,它只需要考虑在这个像素之上的所有 layer 中对应的单个像素,并把这些像素合并到一起。或者,如果最顶层的纹理是不透明的(即图层树的最底层),这时候 GPU 就可以简单的拷贝它的像素到屏幕上。
当一个 layer 上所有的像素和屏幕上的像素完美的对应整齐,那这个 layer 就是像素对齐的。主要有两个原因可能会造成不对齐。第一个便是缩放;当一个纹理放大缩小的时候,纹理的像素便不会和屏幕的像素排列对齐。另一个原因便是当纹理的起点不在一个像素的边界上。
在这两种情况下,GPU 需要再做额外的计算。它需要将源纹理上多个像素混合起来,生成一个用来合成的值。当所有的像素都是对齐的时候,GPU 只剩下很少的工作要做。Core Animation 工具和模拟器有一个叫做 color misaligned images 的选项,当这些在你的 CALayer 实例中发生的时候,这个功能便可向你展示。
一个图层可以有一个和它相关联的 mask(蒙板),mask 是一个拥有 alpha 值的位图,当像素要和它下面包含的像素合并之前都会把 mask 应用到图层的像素上去。当你要设置一个图层的圆角半径时,你可以有效的在图层上面设置一个 mask。但是也可以指定任意一个蒙板。比如,一个字母 A 形状的 mask。最终只有在 mask 中显示出来的(即图层中的部分)才会被渲染出来。
离屏渲染(Offscreen Rendering)
离屏渲染可以被 Core Animation 自动触发,或者被应用程序强制触发。屏幕外的渲染会合并/渲染图层树的一部分到一个新的缓冲区,然后该缓冲区被渲染到屏幕上。
离屏渲染合成计算是非常昂贵的, 但有时你也许希望强制这种操作。一种好的方法就是缓存合成的纹理/图层。如果你的渲染树非常复杂(所有的纹理,以及如何组合在一起),你可以强制离屏渲染缓存那些图层,然后可以用缓存作为合成的结果放到屏幕上。
如果你的程序混合了很多图层,并且想要他们一起做动画,GPU 通常会为每一帧(1/60s)重复合成所有的图层。当使用离屏渲染时,GPU 第一次会混合所有图层到一个基于新的纹理的位图缓存上,然后使用这个纹理来绘制到屏幕上。现在,当这些图层一起移动的时候,GPU 便可以复用这个位图缓存,并且只需要做很少的工作。需要注意的是,只有当那些图层不改变时,这才可以用。如果那些图层改变了,GPU 需要重新创建位图缓存。你可以通过设置 shouldRasterize 为 YES 来触发这个行为。
然而,这是一个权衡。第一,这可能会使事情变得更慢。创建额外的屏幕外缓冲区是 GPU 需要多做的一步操作,特殊情况下这个位图可能再也不需要被复用,这便是一个无用功了。然而,可以被复用的位图,GPU 也有可能将它卸载了。所以你需要计算 GPU 的利用率和帧的速率来判断这个位图是否有用。
离屏渲染也可能产生副作用。如果你正在直接或者间接的将mask应用到一个图层上,Core Animation 为了应用这个 mask,会强制进行屏幕外渲染。这会对 GPU 产生重负。通常情况下 mask 只能被直接渲染到帧的缓冲区中(在屏幕内)。
Instrument 的 Core Animation 工具有一个叫做 Color Offscreen-Rendered Yellow 的选项,它会将已经被渲染到屏幕外缓冲区的区域标注为黄色(这个选项在模拟器中也可以用)。同时记得检查 Color Hits Green and Misses Red 选项。绿色代表无论何时一个屏幕外缓冲区被复用,而红色代表当缓冲区被重新创建。
一般情况下,你需要避免离屏渲染,因为这是很大的消耗。直接将图层合成到帧的缓冲区中(在屏幕上)比先创建屏幕外缓冲区,然后渲染到纹理中,最后将结果渲染到帧的缓冲区中要廉价很多。因为这其中涉及两次昂贵的环境转换(转换环境到屏幕外缓冲区,然后转换环境到帧缓冲区)。
所以当你打开 Color Offscreen-Rendered Yellow 后看到黄色,这便是一个警告,但这不一定是不好的。如果 Core Animation 能够复用屏幕外渲染的结果,这便能够提升性能。
同时还要注意,rasterized layer 的空间是有限的。苹果暗示大概有屏幕大小两倍的空间来存储 rasterized layer/屏幕外缓冲区。
如果你使用 layer 的方式会通过屏幕外渲染,你最好摆脱这种方式。为 layer 使用蒙板或者设置圆角半径会造成屏幕外渲染,产生阴影也会如此。
至于 mask,圆角半径(特殊的mask)和 clipsToBounds/masksToBounds,你可以简单的为一个已经拥有 mask 的 layer 创建内容,比如,已经应用了 mask 的 layer 使用一张图片。如果你想根据 layer 的内容为其应用一个长方形 mask,你可以使用 contentsRect 来代替蒙板。如果你最后设置了 shouldRasterize 为 YES,那也要记住设置 rasterizationScale 为 contentsScale。
Core Animation OpenGL ES
Core Animation 让你在屏幕上实现动画。我们将跳过动画部分,而集中在绘图上。需要注意的是,Core Animation 允许你做非常高效的渲染。这也是为什么当你使用 Core Animation 时可以实现每秒 60 帧的动画。
Core Animation 的核心是 OpenGL ES 的一个抽象物,简而言之,它让你直接使用 OpenGL ES 的功能,却不需要处理 OpenGL ES 做的复杂的事情。当我们上面谈论合成的时候,我们把 layer 和 texture 当做等价的,但是他们不是同一物体,可又是如此的类似。
Core Animation 的 layer 可以有子 layer,所以最终你得到的是一个图层树。Core Animation 所需要做的最繁重的任务便是判断出哪些图层需要被(重新)绘制,而 OpenGL ES 需要做的便是将图层合并、显示到屏幕上。
举个例子,当你设置一个 layer 的内容为 CGImageRef 时,Core Animation 会创建一个 OpenGL 纹理,并确保在这个图层中的位图被上传到对应的纹理中。以及当你重写 -drawInContext 方法时,Core Animation 会请求分配一个纹理,同时确保 Core Graphics 会将你所做的(即你在drawInContext中绘制的东西)放入到纹理的位图数据中。一个图层的性质和 CALayer 的子类会影响到 OpenGL 的渲染结果,许多低等级的 OpenGL ES 行为被简单易懂地封装到 CALayer 概念中。
Core Animation 通过 Core Graphics 的一端和 OpenGL ES 的另一端,精心策划基于 CPU 的位图绘制。因为 Core Animation 处在渲染过程中的重要位置上,所以你如何使用 Core Animation 将会对性能产生极大的影响。
CPU 限制 VS GPU 限制
当你在屏幕上显示东西的时候,有许多组件参与了其中的工作。其中,CPU 和 GPU 在硬件中扮演了重要的角色。在他们命名中 P 和 U 分别代表了”处理”和”单元”,当需要在屏幕上进行绘制时,他们都需要做处理,同时他们都有资源限制(即 CPU 和 GPU 的硬件资源)。
为了每秒达到 60 帧,你需要确定 CPU 和 GPU 不能过载。此外,即使你当前能达到 60fps(frame per second),你还是要把尽可能多的绘制工作交给 GPU 做,而让 CPU 尽可能的来执行应用程序。通常,GPU 的渲染性能要比 CPU 高效很多,同时对系统的负载和消耗也更低一些。
既然绘图性能是基于 CPU 和 GPU 的,那么你需要找出是哪一个限制你绘图性能的。如果你用尽了 GPU 所有的资源,也就是说,是 GPU 限制了你的性能,同样的,如果你用尽了 CPU,那就是 CPU 限制了你的性能。
要告诉你,如果是 GPU 限制了你的性能,你可以使用 OpenGL ES Driver instrument。点击上面那个小的 i 按钮,配置一下,同时注意勾选 Device Utilization %。现在,当你运行你的 app 时,你可以看到你 GPU 的负荷。如果这个值靠近 100%,那么你就需要把你工作的重心放在GPU方面了。