前言html
一个像素是如何绘制到屏幕上去的?有不少种方式将一些东西映射到显示屏上,他们须要调用不一样的框架、许多功能和方法的结合体。这里咱们大概的看一下屏幕以后发生的事情。当你想要弄清楚何时、怎么去查明并解决问题时,我但愿这篇文章能帮助你理解哪个 API 能够更好的帮你解决问题。咱们将聚焦于 iOS,然而我讨论的大多数问题也一样适用于 OS X。ios
目录程序员
1. 图形堆栈
2. 软件组成
3. 硬件参与者
4. 合成
5. 不透明 VS 透明
6. 像素对齐 VS 不重合在一块儿
7. Masks
8. 离屏渲染(Offscreen Rendering)
9. 更多的关于合成
10. Core Animation OpenGL ES
11. CPU限制 VS GPU限制
12. Core Graphics / Quartz 2D
13. CGLayer
14. 像素
15. 默认的像素布局
16. 深奥的布局
17. 二维数据
18. YCbCr
19. 图片格式
20. UIKit 和 Pixels
21. 绘图与图层CALayer算法
1. 图形堆栈数组
当像素映射到屏幕上的时候,后台发生了不少事情。但一旦他们显示到屏幕上,每个像素均由三个颜色组件构成:红,绿,蓝。三个独立的颜色单元会根据给定的颜色显示到一个像素上。在 iPhone5 的液晶显示器上有1,136×640=727,040个像素,所以有2,181,120个颜色单元。在15寸视网膜屏的 MacBook Pro 上,这一数字达到15.5百万以上。全部的图形堆栈一块儿工做以确保每次正确的显示。当你滚动整个屏幕的时候,数以百万计的颜色单元必须以每秒60次的速度刷新,这是一个很大的工做量。缓存
2. 软件组成安全
从简单的角度来看,软件堆栈看起来有点像这样:session
Display 的上一层即是图形处理单元 GPU,GPU 是一个专门为图形高并发计算而量身定作的处理单元。这也是为何它能同时更新全部的像素,并呈现到显示器上。它并发的本性让它能高效的将不一样纹理合成起来。咱们将有一小块内容来更详细的讨论图形合成。关键的是,GPU 是很是专业的,所以在某些工做上很是高效。好比,GPU 很是快,而且比 CPU 使用更少的电来完成工做。一般 CPU 都有一个广泛的目的,它能够作不少不一样的事情,可是合成图像在 CPU 上却显得比较慢。 并发
GPU Driver 是直接和 GPU 交流的代码块。不一样的GPU是不一样的性能怪兽,可是驱动使他们在下一个层级上显示的更为统一,典型的下一层级有 OpenGL/OpenGL ES. app
OpenGL(Open Graphics Library) 是一个提供了 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 框架,和其余一些混合的入口。
要记住一件事情,GPU 是一个很是强大的图形硬件,而且在显示像素方面起着核心做用。它链接到 CPU。从硬件上讲二者之间存在某种类型的总线,而且有像 OpenGL,Core Animation 和 Core Graphics 这样的框架来在 GPU 和 CPU 之间精心安排数据的传输。为了将像素显示到屏幕上,一些处理将在 CPU 上进行。而后数据将会传送到 GPU,这也须要作一些相应的操做,最终像素显示到屏幕上。
这个过程的每一部分都有各自的挑战,而且许多时候须要作出折中的选择。
3. 硬件参与者
正如上面这张简单的图片显示那些挑战: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。
这张图涉及到一些错综复杂的方面,咱们将会把这些方面提取出来并深一步了解。
4. 合成
在图形世界中,合成是一个描述不一样位图如何放到一块儿来建立你最终在屏幕上看到图像的过程。在许多方面显得显而易见,而让人忘了背后错综复杂的计算。
让咱们忽略一些难懂的事例而且假定屏幕上一切事物皆纹理。一个纹理就是一个包含 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),公式看起来便会像这样:
0.5 0 0.5 R = S + D * (1 - Sa) = 0 + 0 * (1 - 0.5) = 0 0 1 0.5
咱们最终获得RGB值为(0.5, 0, 0.5),是一个紫色。这正是咱们所指望将透明红色合成到蓝色背景上所获得的。
记住咱们刚刚只是将纹理中的一个像素合成到另外一个纹理的像素上。当两个纹理覆盖在一块儿的时候,GPU须要为全部像素作这种操做。正如你所知道的同样,许多程序都有不少层,所以全部的纹理都须要合成到一块儿。尽管GPU是一块高度优化的硬件来作这种事情,但这仍是会让它很是忙碌。
5. 不透明 VS 透明
当源纹理是彻底不透明的时候,目标像素就等于源纹理。这能够省下 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 通道。
6. 像素对齐 VS 不重合在一块儿
到如今咱们都在考虑像素完美重合在一块儿的 layers。当全部的像素是对齐的时候咱们获得相对简单的计算公式。每当 GPU 须要计算出屏幕上一个像素是什么颜色的时候,它只须要考虑在这个像素之上的全部 layer 中对应的单个像素,并把这些像素合并到一块儿。或者,若是最顶层的纹理是不透明的(即图层树的最底层),这时候 GPU 就能够简单的拷贝它的像素到屏幕上。
当一个 layer 上全部的像素和屏幕上的像素完美的对应整齐,那这个 layer 就是像素对齐的。主要有两个缘由可能会形成不对齐。第一个即是滚动;当一个纹理上下滚动的时候,纹理的像素便不会和屏幕的像素排列对齐。另外一个缘由即是当纹理的起点不在一个像素的边界上。
在这两种状况下,GPU 须要再作额外的计算。它须要将源纹理上多个像素混合起来,生成一个用来合成的值。当全部的像素都是对齐的时候,GPU 只剩下不多的工做要作。
Core Animation 工具和模拟器有一个叫作 color misaligned images 的选项,当这些在你的 CALayer 实例中发生的时候,这个功能即可向你展现。
7. Masks
一个图层能够有一个和它相关联的 mask(蒙板),mask 是一个拥有 alpha 值的位图,当像素要和它下面包含的像素合并以前都会把 mask 应用到图层的像素上去。当你要设置一个图层的圆角半径时,你能够有效的在图层上面设置一个 mask。可是也能够指定任意一个蒙板。好比,一个字母 A 形状的 mask。最终只有在 mask 中显示出来的(即图层中的部分)才会被渲染出来。
8. 离屏渲染(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。
9. 更多的关于合成
像往常同样,维基百科上有更多关于透明合成的基础公式。当咱们谈完像素后,咱们将更深刻一点的谈论红,绿,蓝和 alpha 是怎么在内存中表现的。
10. 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 将会对性能产生极大的影响。
11. 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方面了。
12. Core Graphics / Quartz 2D
经过 Core Graphics 这个框架,Quartz 2D 被更为普遍的知道。
Quartz 2D 拥有比咱们这里谈到更多的装饰。咱们这里不会过多的讨论关于 PDF 的建立,渲染,解析,或者打印。只须要注意的是,PDF 的打印、建立和在屏幕上绘制位图的操做是差很少的。由于他们都是基于 Quartz 2D。
让咱们简单的了解一下 Quartz 2D 主要的概念。有关详细信息能够到苹果的官方文档中了解。
放心,当 Quartz 2D 涉及到 2D 绘制的时候,它是很是强大的。有基于路径的绘制,反锯齿渲染,透明图层,分辨率,而且设备独立,能够说出不少特点。这可能会让人产生畏惧,主要由于这是一个低级而且基于 C 的 API。
主要的概念相对简单,UIKit 和 AppKit 都包含了 Quartz 2D 的一些简单 API,一旦你熟练了,一些简单 C 的 API 也是很容易理解的。最终你学会了一个能实现 Photoshop 和 Illustrator 大部分功能的绘图引擎。苹果把 iOS 程序里面的股票应用做为讲解 Quartz 2D 在代码中实现动态渲染的一个例子。
当你的程序进行位图绘制时,无论使用哪一种方式,都是基于 Quartz 2D 的。也就是说,CPU 部分实现的绘制是经过 Quartz 2D 实现的。尽管 Quartz 能够作其它的事情,可是咱们这里仍是集中于位图绘制,在缓冲区(一块内存)绘制位图会包括 RGBA 数据。
比方说,咱们要画一个八角形,咱们经过 UIKit 能作到这一点
相对应的 Core Graphics 代码:
须要问的问题是:这个绘制到哪儿去了?这正好引出所谓的 CGContext 登场。咱们传过去的ctx参数正是在那个上下文中。而这个上下文定义了咱们须要绘制的地方。若是咱们实现了 CALayer 的 -drawInContext:
这时已经传过来一个上下文。绘制到这个上下文中的内容将会被绘制到图层的备份区(图层的缓冲区).可是咱们也能够建立咱们本身的上下文,叫作基于位图的上下文,好比CGBitmapContextCreate()
.这个方法返回一个咱们能够传给 CGContext 方法来绘制的上下文。
注意 UIKit 版本的代码为什么不传入一个上下文参数到方法中?这是由于当使用 UIKit 或者 AppKit 时,上下文是惟一的。UIkit 维护着一个上下文堆栈,UIKit 方法老是绘制到最顶层的上下文中。你可使用 UIGraphicsGetCurrentContext()
来获得最顶层的上下文。你可使用 UIGraphicsPushContext()
和 UIGraphicsPopContext()
在 UIKit 的堆栈中推动或取出上下文。
最为突出的是,UIKit 使用 UIGraphicsBeginImageContextWithOptions()
和 UIGraphicsEndImageContext()
方便的建立相似于 CGBitmapContextCreate()
的位图上下文。混合调用 UIKit 和 Core Graphics 很是简单:
或者另一种方法:
你可使用 Core Graphics 建立大量的很是酷的东西。一个很好的理由就是,苹果的文档有不少例子。咱们不能获得全部的细节,可是 Core Graphics 有一个很是接近 Adobe Illustrator 和 Adobe Photoshop 如何工做的绘图模型,而且大多数工具的理念翻译成 Core Graphics 了。终究,他是起源于 NeXTSTEP 。(原来也是乔老爷的做品)。
13. CGLayer
咱们最初指出 CGLayer 能够用来提高重复绘制相同元素的速度。正如 Dave Hayden指出,这些小道消息再也不可靠。
14. 像素
屏幕上的像素是由红,绿,蓝三种颜色组件构成的。所以,位图数据有时也被叫作 RGB 数据。你可能会对数据如何组织在内存中感到好奇。而事实是,有不少种不一样的方式在内存中展示RGB位图数据。
稍后咱们将会谈到压缩数据,这又是一个彻底不一样的概念。如今,咱们先看一下RGB位图数据,咱们能够从颜色组件:红,绿,蓝中获得一个值。而大多数状况下,咱们有第四个组件:透明度。最终咱们从每一个像素中获得四个单独的值。
15. 默认的像素布局
在 iOS 和 OS X 上最多见的格式就是你们所熟知的 32bits-per-pixel(bpp), 8bits-per-componet(bpc),透明度会首先被乘以到像素值上(就像上文中提到的那个公式同样),在内存中,像下面这样:
A R G B A R G B A R G B | pixel 0 | pixel 1 | pixel 2 0 1 2 3 4 5 6 7 8 9 10 11 ...
这个格式常常被叫作 ARGB。每一个像素占用 4 字节(32bpp),每个颜色组件是1字节(8bpc).每一个像素有一个 alpha 值,这个值老是最早获得的(在RGB值以前),最终红、绿、蓝的值都会被预先乘以 alpha 的值。预乘的意思就是 alpha 值被烘烤到红、绿、蓝的组件中。若是咱们有一个橙色,他们各自的 8bpc 就像这样: 240,99,24.一个彻底不透明的橙色像素拥有的 ARGB 值为: 255,240,99,24,它在内存中的布局就像上面图示那样。若是咱们有一个相同颜色的像素,可是 alpha 值为 33%,那么他的像素值即是:84,80,33,8.
另外一个常见的格式即是 32bpp,8bpc,跳过第一个 alpha 值,看起来像下面这样:
x R G B x R G B x R G B | pixel 0 | pixel 1 | pixel 2 0 1 2 3 4 5 6 7 8 9 10 11 ...
这常被叫作 xRGB。像素并无任何 alpha 值(他们都被假定为100%不透明),可是内存布局是同样的。你应该想知道为何这种格式很流行,当咱们每个像素中都有一个不用字节时,咱们将会省下 25% 的空间。事实证实,这种格式更容易被现代的 CPU 和绘图算法消化,由于每个独立的像素都对齐到 32-bit 的边界。现代的 CPU 不喜欢装载(读取)不对齐的数据,特别是当将这种数据和上面没有 alpha 值格式的数据混合时,算法须要作不少挪动和蒙板操做。
当处理 RGB 数据时,Core Graphics 也须要支持把alpha 值放到最后(另外还要支持跳过)。有时候也分别称为 RGBA 和 RGBx,假定是 8bpc,而且预乘了 alpha 值。
16. 深奥的布局
大多数时候,当处理位图数据时,咱们也须要处理 Core Graphics/Quartz 2D。有一个很是详细的列表列出了他支持的混合组合。可是让咱们首先看一下剩下的 RGB 格式:
另外一个选择是 16bpp,5bpc,不包含 alpha 值。这个格式相比以前一个仅占用 50% 的存储大小(每一个像素2字节),但将使你存储它的 RGB 数据到内存或磁盘中变得困难。既然这种格式中,每一个颜色组件只有 5bits(原文中写的是每一个像素是5bits,但根据上下文可知应该是每一个组件),这样图形(特别是平滑渐变的)会形成重叠在一块儿的假象。
还有一个是 64bpp,16bpc,最终为 128bpp,32bpc,浮点数组件(有或没有 alpha 值)。它们分别使用 8 字节和 16 字节,而且容许更高的精度。固然,这会形成更多的内存使用和昂贵的计算。
整件事件中,Core Graphics 也支持一些像灰度模式和 CMYK 格式,这些格式相似于仅有 alpha 值的格式(蒙板)。
17. 二维数据
当颜色组件(红、绿、蓝、alpha)混杂在一块儿的时候,大多数框架(包括 Core Graphics )使用像素数据。正是这种状况下咱们称之为二维数据,或者二维组件。这个意思是:每个颜色组件都在它本身的内存区域,也就是说它是二维的。好比 RGB 数据,咱们有三个独立的内存区域,一个大的区域包含了全部像素的红颜色的值,一个包含了全部绿颜色的值,一个包含了全部蓝颜色的值。
在某些状况下,一些视频框架便会使用二维数据。
18. YCbCr
当咱们处理视频数据时,YCbCr 是一种常见的格式。它也是包含了三种(Y,Cb和Cr)表明颜色数据的组件。可是简单的讲,它更相似于经过人眼看到的颜色。人眼对 Cb 和 Cr 这两种组件的色彩度不太能精确的辨认出来,可是能很准确的识别出 Y 的亮度。当数据使用 YCbCr 格式时,在同等的条件下,Cb 和 Cr 组件比 Y 组件压缩的更紧密。
出于一样的缘由,JPEG 图像有时会将像素数据从 RGB 转换到 YCbCr。JPEG 单独的压缩每个二维颜色。当压缩基于 YCbCr 的平面时,Cb 和 Cr 能比 Y 压缩得更彻底。
19. 图片格式
当你在 iOS 或者 OS X 上处理图片时,他们大多数为 JPEG 和 PNG。让咱们更进一步观察。
1) JPEG
每一个人都知道 JPEG。它是相机的产物。它表明着照片如何存储在电脑上。甚至你妈妈都据说过 JPEG。
一个很好的理由,不少人都认为 JPEG 文件仅是另外一种像素数据的格式,就像咱们刚刚谈到的 RGB 像素布局那样。这样理解离真相真是差十万八千里了。
将 JPEG 数据转换成像素数据是一个很是复杂的过程,你经过一个周末的计划都不能完成,甚至是一个很是漫长的周末(原文的意思好像就是为了表达这个过程很是复杂,不过老外的比喻总让人拎不清)。对于每个二维颜色,JPEG 使用一种基于离散余弦变换(简称 DCT 变换)的算法,将空间信息转变到频域.这个信息而后被量子化,排好序,而且用一种哈夫曼编码的变种来压缩。不少时候,首先数据会被从 RGB 转换到二维 YCbCr,当解码 JPEG 的时候,这一切都将变得可逆。
这也是为何当你经过 JPEG 文件建立一个 UIImage 而且绘制到屏幕上时,将会有一个延时,由于 CPU 这时候忙于解压这个 JPEG。若是你须要为每个 tableviewcell 解压 JPEG,那么你的滚动固然不会平滑(原来 tableviewcell 里面最要不要用 JPEG 的图片)。
那究竟为何咱们还要用 JPEG 呢?答案就是 JPEG 能够很是很是好的压缩图片。一个经过 iPhone5 拍摄的,未经压缩的图片占用接近 24M。可是经过默认压缩设置,你的照片一般只会在 2-3M 左右。JPEG 压缩这么好是由于它是失真的,它去除了人眼很难察觉的信息,而且这样作能够超出像 gzip 这样压缩算法的限制。但这仅仅在图片上有效的,由于 JPEG 依赖于图片上有不少人类不能察觉出的数据。若是你从一个基本显示文本的网页上截取一张图,JPEG 将不会这么高效。压缩效率将会变得低下,你甚至能看出来图片已经压缩变形了。
2) PNG
PNG读做”ping”。和 JPEG 相反,它的压缩对格式是无损的。当你将一张图片保存为 PNG,而且打开它(或解压),全部的像素数据会和最初如出一辙,由于这个限制,PNG 不能像 JPEG 同样压缩图片,可是对于像程序中的原图(如buttons,icons),它工做的很是好。更重要的是,解码 PNG 数据比解码 JPEG 简单的多。
在现实世界中,事情历来没有那么简单,目前存在了大量不一样的 PNG 格式。能够经过维基百科查看详情。可是简言之,PNG 支持压缩带或不带 alpha 通道的颜色像素(RGB),这也是为何它在程序原图中表现良好的另外一个缘由。
3) 挑选一个格式
当你在你的程序中使用图片时,你须要坚持这两种格式: JPEG 或者 PNG。读写这种格式文件的压缩和解压文件能表现出很高的性能,另外,还支持并行操做。同时 Apple 正在改进解压缩并可能出如今未来的新操做系统中,届时你将会获得持续的性能提高。若是尝试使用另外一种格式,你须要注意到,这可能对你程序的性能会产生影响,同时可能会打开安全漏洞,常常,图像解压缩算法是黑客最喜欢的攻击目标。
已经写了不少关于优化 PNGs,若是你想要了解更多,请到互联网上查询。很是重要的一点,注意 Xcode 优化 PNG 选项和优化其余引擎有很大的不一样。
当 Xcode 优化一个 PNG 文件的时候,它将 PNG 文件变成一个从技术上讲再也不是有效的PNG文件。可是 iOS 能够读取这种文件,而且这比解压缩正常的 PNG 文件更快。Xcode 改变他们,让 iOS 经过一种对正常 PNG 不起做用的算法来对他们解压缩。值得注意的重点是,这改变了像素的布局。正如咱们所提到的同样,在像素之下有不少种方式来描绘 RGB 数据,若是这不是 iOS 绘制系统所须要的格式,它须要将每个像素的数据替换,而不须要加速来作这件事。
让咱们再强调一遍,若是你能够,你须要为原图设置 resizable images。你的文件将变得更小,所以你只须要从文件系统装载更少的数据。
20. UIKit 和 Pixels
每个在 UIKit 中的 view 都有它本身的 CALayer。依次,这些图层都有一个叫像素位图的后备存储,有点像一个图像。这个后备存储正是被渲染到显示器上的。
1) 使用 –drawRect:
若是你的视图类实现了 -drawRect:
,他们将像这样工做:
当你调用 -setNeedsDisplay
,UIKit 将会在这个视图的图层上调用 -setNeedsDisplay
。这为图层设置了一个标识,标记为 dirty(直译是脏的意思,想不出用什么词比较贴切,污染?),但还显示原来的内容。它实际上没作任何工做,因此屡次调用 -setNeedsDisplay
并不会形成性能损失。
下面,当渲染系统准备好,它会调用视图图层的-display方法.此时,图层会装配它的后备存储。而后创建一个 Core Graphics 上下文(CGContextRef),将后备存储对应内存中的数据恢复出来,绘图会进入对应的内存区域,并使用 CGContextRef 绘制。
当你使用 UIKit 的绘制方法,例如: UIRectFill()
或者 -[UIBezierPath fill]
代替你的 -drawRect:
方法,他们将会使用这个上下文。使用方法是,UIKit 将后备存储的 CGContextRef 推动他的 graphics context stack,也就是说,它会将那个上下文设置为当前的。所以 UIGraphicsGetCurrent()
将会返回那个对应的上下文。既然 UIKit 使用 UIGraphicsGetCurrent()
绘制方法,绘图将会进入到图层的后备存储。若是你想直接使用 Core Graphics 方法,你能够本身调用 UIGraphicsGetCurrent()
获得相同的上下文,而且将这个上下文传给 Core Graphics 方法。
从如今开始,图层的后备存储将会被不断的渲染到屏幕上。直到下次再次调用视图的 -setNeedsDisplay
,将会依次将图层的后备存储更新到视图上。
2) 不使用 -drawRect:
当你用一个 UIImageView 时,事情略有不一样,这个视图仍然有一个 CALayer,可是图层却没有申请一个后备存储。取而代之的是使用一个 CGImageRef 做为他的内容,而且渲染服务将会把图片的数据绘制到帧的缓冲区,好比,绘制到显示屏。
在这种状况下,将不会继续从新绘制。咱们只是简单的将位图数据以图片的形式传给了 UIImageView,而后 UIImageView 传给了 Core Animation,而后轮流传给渲染服务。
3) 实现-drawRect: 仍是不实现 -drawRect:
这听起来貌似有点低俗,可是最快的绘制就是你不要作任何绘制。
大多数时间,你能够不要合成你在其余视图(图层)上定制的视图(图层),这正是咱们推荐的,由于 UIKit 的视图类是很是优化的 (就是让咱们不要闲着没事作,本身去合并视图或图层) 。
当你须要自定义绘图代码时,Apple 在WWDC 2012’s session 506:Optimizing 2D Graphics and Animation Performance 中展现了一个很好的例子:”finger painting”。
另外一个地方须要自定义绘图的就是 iOS 的股票软件。股票是直接用 Core Graphics 在设备上绘制的,注意,这仅仅是你须要自定义绘图,你并不须要实现 -drawRect:
方法。有时,经过 UIGraphicsBeginImageContextWithOptions()
或者CGBitmapContextCeate()
建立位图会显得更有意义,从位图上面抓取图像,并设置为 CALayer
的内容。下面咱们将给出一个例子来测试,检验。
4) 单一颜色
若是咱们看这个例子:
如今咱们知道这为何很差:咱们促使 Core Animation 来为咱们建立一个后备存储,并让它使用单一颜色填充后备存储,而后上传给 GPU。
咱们跟本不须要实现 -drawRect:
,并节省这些代码工做量,只需简单的设置这个视图图层的背景颜色。若是这个视图有一个 CAGradientLayer 做为图层,那么这个技术也一样适用于此(渐变图层)。
5) 可变尺寸的图像
相似的,你可使用可变尺寸的图像来下降绘图系统的压力。让咱们假设你须要一个 300×50 点的按钮插图,这将是 600×100=60k 像素或者 60kx4=240kB 内存大小须要上传到 GPU,而且占用 VRAM。若是咱们使用所谓的可变尺寸的图像,咱们只须要一个 54×12 点的图像,这将占用低于 2.6k 的像素或者 10kB 的内存,这样就变得更快了。
Core Animation 能够经过 CALayer 的 contentsCenter
属性来改变图像,大多数状况下,你可能更倾向于使用,-[UIImage resizableImageWithCapInsets:resizingMode:]
。
同时注意,在第一次渲染这个按钮以前,咱们并不须要从文件系统读取一个 60k 像素的 PNG 并解码,解码一个小的 PNG 将会更快。经过这种方式,你的程序在每一步的调用中都将作更少的工做,而且你的视图将会加载的更快。
21. 绘图与图层CALayer
1) 并发绘图
UIKit 的线程模型是很是简单的:你仅能够从主队列(好比主线程)中调用 UIKit 类(好比视图),那么并发绘图又是什么呢?
若是你必须实现 -drawRect:
,而且你必须绘制大量的东西,这将占用时间。因为你但愿动画变得更平滑,除了在主队列中,你还但愿在其余队列中作一些工做。同时发生的绘图是复杂的,可是除了几个警告,同时发生的绘图仍是比较容易实现的。
咱们除了在主队列中能够向 CALayer 的后备存储中绘制一些东西,其余方法都将不可行。可怕的事情将会发生。咱们能作的就是向一个彻底断开连接的位图上下文中进行绘制。
正如咱们上面所提到的同样,在 Core Graphics 下,全部 Core Graphics 绘制方法都须要一个上下文参数来指定绘制到那个上下文中。UIKit 有一个当前上下文的概念(也就是绘制到哪儿去)。这个当前的上下文就是 per-thread.
为了同时绘制,咱们须要作下面的操做。咱们须要在另外一个队列建立一个图像,一旦咱们拥有了图像,咱们能够切换回主队列,而且设置这个图像为 UIImageView 的图像。这个技术在 WWDC 2012 session 211 中讨论过。(异步下载图片常常用到这个)
增长一个你能够在其中绘制的新方法:
这个方法经过 UIGraphicsBeginImageContextWithOptions()
方法,并根据给定的大小建立一个新的 CGContextRef 位图。这个方法也会将这个上下文设置为当前UIKit的上下文。如今你能够在这里作你想在 -drawRect:
中作的事了。而后咱们能够经过UIGraphicsGetImageFromCurrentImageContext()
,将得到的这个上下文位图数据做为一个 UIImage,最终移除这个上下文。
很重要的一点就是,你在这个方法中所作的全部绘图的代码都是线程安全的,也就是说,当你访问属性等等,他们须要线程安全。由于你是在另外一个队列中调用这个方法的。若是这个方法在你的视图类中,那就须要注意一点了。另外一个选择就是建立一个单独的渲染类,并设置全部须要的属性,而后经过触发来渲染图片。若是这样,你能够经过使用简单的 UIImageView 或者 UITableViewCell。
要知道,全部 UIKit 的绘制 API 在使用另外一个队列时,都是安全的。只须要肯定是在同一个操做中调用他们的,这个操做须要以UIGraphicsBeginImageContextWithOptions()
开始,以 UIGraphicsEndIamgeContext()
结束。
你须要像下面这样触发渲染代码:
要注意,咱们是在主队列中调用 view.image = image.这是一个很是重要的细节。你不能够在任何其余队列中调用这个代码。
像往常同样,同时绘制会伴随不少问题,你如今须要取消后台渲染。而且在渲染队列中设置合理的同时绘制的最大限度。
为了支持这一切,最简单的就是在一个 NSOperation 子类内部实现 -renderInImageOfSize:
。
最终,须要指出,设置 UITableViewCell 内容为异步是很是困难的。单元格颇有可能在完成异步渲染前已经被复用了。尽管单元格已经被其余地方复用,但你只须要设置内容就好了。
2)CALayer
到如今为止,你须要知道在 GPU 内,一个 CALayer 在某种方式上和一个纹理相似。图层有一个后备存储,这即是被用来绘制到屏幕上的位图。
一般,当你使用 CALayer 时,你会设置它的内容为一个图片。这到底作了什么?这样作会告诉 Core Animation 使用图片的位图数据做为纹理。若是这个图片(JPEG或PNG)被压缩了,Core Animation 将会这个图片解压缩,而后上传像素数据到 GPU。
尽管还有不少其余种类的图层,若是你是用一个简单的没有设置上下文的 CALayer,并为这个 CALayer 设置一个背景颜色,Core Animation 并不会上传任何数据到 GPU,但却可以不用任何像素数据而在 GPU 上完成全部的工做,相似的,对于渐变的图层,GPU 是能建立渐变的,并且不须要 CPU 作任何工做,而且不须要上传任何数据到 GPU。
3)自定义绘制的图层
若是一个 CALayer 的子类实现了 -drawInContext:
或者它的代理,相似于 -drawLayer:inContest:
, Core Animation 将会为这个图层申请一个后备存储,用来保存那些方法绘制进来的位图。那些方法内的代码将会运行在 CPU 上,结果将会被上传到 GPU。
4)形状和文本图层
形状和文本图层仍是有些不一样的。开始时,Core Animation 为这些图层申请一个后备存储来保存那些须要为上下文生成的位图数据。而后 Core Animation 会将这些图形或文本绘制到后备存储上。这在概念上很是相似于,当你实现 -drawInContext:
方法,而后在方法内绘制形状或文本,他们的性能也很接近。
在某种程度上,当你须要改变形状或者文本图层时,这须要更新它的后备存储,Core Animation 将会从新渲染后备存储。例如,当动态改变形状图层的大小时,Core Animation 须要为动画中的每一帧从新绘制形状。
5)异步绘图
CALayer 有一个叫作 drawsAsynchronously 的属性,这彷佛是一个解决全部问题的高招。注意,尽管这可能提高性能,但也可能让事情变慢。
当你设置 drawsAsynchronously 为 YES 时,发生了什么?你的 -drawRect:/-drawInContext:
方法仍然会被在主线程上调用。可是全部调用 Core Graphics 的操做都不会被执行。取而代之的是,绘制命令被推迟,而且在后台线程中异步执行。
这种方式就是先记录绘图命令,而后在后台线程中重现。为了这个过程的顺利进行,更多的工做须要被作,更多的内存须要被申请。可是主队列中的一些工做便被移出来了(大概意思就是让咱们把一些能在后台实现的工做放到后台实现,让主线程更顺畅)。
对于昂贵的绘图方法,这是最有可能提高性能的,但对于那些绘图方法来讲,也不会节省太多资源。