绘制像素到屏幕上 - 学习

原文http://www.objccn.io/issue-3-1/

以下是自己的学习笔记。文章很不错,强烈建议去看原文,值得学习。

软件组成

从软件角度看,App中的内容是如何绘制到屏幕上的:

image

要记住一件事情,GPU 是一个非常强大的图形硬件,并且在显示像素方面起着核心作用。它连接到 CPU。从硬件上讲两者之间存在某种类型的总线,并且有像 OpenGL,Core Animation 和 Core Graphics 这样的框架来在 GPU 和 CPU 之间精心安排数据的传输。为了将像素显示到屏幕上,一些处理将在 CPU 上进行。然后数据将会传送到 GPU,这也需要做一些相应的操作,最终像素显示到屏幕上。

这个过程的每一部分都有各自的挑战,并且许多时候需要做出折中的选择。

硬件参与者

从硬件角度看,假如需要显示一张PNG图片到屏幕上:

  1. CPU从bundle中加载一张PNG图片并且解压它(存储在RAM中)
  2. 将解压缩后的图片,通过某种方式上传到GPU;(从RAM移动到VRAM上)
  3. GPU将每一个frame的纹理合成在一起(一秒60次);

一些看似平凡的,比如显示文本,对 CPU 来说却是一件非常复杂的事情,这会促使 Core Text 和 Core Graphics 框架更紧密的集成来根据文本生成一个位图。一旦准备好,它将会被作为一个纹理上传到 GPU 并准备显示出来。当你滚动或者在屏幕上移动文本时,不管怎么样,同样的纹理能够被复用,CPU 只需简单的告诉 GPU 新的位置就行了,所以 GPU 就可以重用存在的纹理了。CPU 并不需要重新渲染文本,并且位图也不需要重新上传到 GPU

image

纹理合成

假定屏幕上一切事物皆纹理,一个纹理就是一个包含 RGBA 值的长方形,比如,每一个像素里面都包含红、绿、蓝和透明度的值。在 Core Animation 世界中这就相当于一个 CALayer。对于屏幕上的每一个像素,GPU 需要混合这些纹理来得到像素 RGB 的值。

合成算法:

R = S + D * ( 1 – Sa )

结果的颜色是源色彩(顶端纹理)+目标颜色(低一层的纹理)*(1-源颜色的透明度)。

显然,当源纹理是完全不透明的时候,目标像素就等于源纹理。这可以省下 GPU 很大的工作量,这样只需简单的拷贝源纹理而不需要合成所有的像素值。如何告诉GPU纹理上的像素是透明还是不透明的?CALayer 有一个叫做 opaque 的属性,如果这个属性为YES, GPU将不会做任何合成,而是简单的拷贝,不需要考虑它下方的任何东西,这会节省GPU大量工作。这也正是 Instruments 中 color blended layers 选项中所涉及的。(这在模拟器中的Debug菜单中也可用).它允许你看到哪一个 layers(纹理) 被标注为透明的,比如 GPU 正在为哪一个 layers 做合成。合成不透明的 layers 因为需要更少的数学计算而更廉价。

当所有像素是对齐的时候,GPU在计算某个点的像素时,只需要考虑这个像素上的所有layer中对应的像素值,将这些像素值进行合成即可。或者,如果最顶层的纹理是不透明的(即图层树的最底层),这时候 GPU 就可以简单的拷贝它的像素到屏幕上。

但是,当像素没有对齐,那么GPU就需要做额外的工作。它需要将源纹理上多个像素混合起来,生成一个用来合成的值。当所有的像素都是对齐的时候,GPU 只剩下很少的工作要做。

一般主要有两个原因可能会造成像素不对其。第一个便是滚动;当一个纹理上下滚动的时候,纹理的像素便不会和屏幕的像素排列对齐。另一个原因便是当纹理的起点不在一个像素的边界上。

在模拟器运行时,模拟器菜单->Debug->Color Misaligned Images,界面中那些紫色或者黄色的区域就是没有对齐到像素。紫色区域显示的字,在真机上可能或出现模糊,黄色区域可能会有变形。

离屏渲染(Offscreen Rendering)

离屏渲染就是先渲染到一个缓冲区,然后再在合适的时机将缓冲区渲染到屏幕上。

离屏渲染合成计算是非常昂贵的, 但有时你也许希望强制这种操作(比如一些需要频繁复用的位图)。一种好的方法就是缓存合成的纹理/图层。如果你的渲染树非常复杂(所有的纹理,以及如何组合在一起),你可以强制离屏渲染缓存那些图层,然后可以用缓存作为合成的结果放到屏幕上。

Instrument 的 Core Animation 工具有一个叫做 Color Offscreen-Rendered Yellow 的选项,它会将已经被渲染到屏幕外缓冲区的区域标注为黄色(这个选项在模拟器中也可以用)。同时记得检查 Color Hits Green and Misses Red 选项。绿色代表无论何时一个屏幕外缓冲区被复用,而红色代表当缓冲区被重新创建。

一般情况下,你需要避免离屏渲染,因为这是很大的消耗。直接将图层合成到帧的缓冲区中(在屏幕上)比先创建屏幕外缓冲区,然后渲染到纹理中,最后将结果渲染到帧的缓冲区中要廉价很多。因为这其中涉及两次昂贵的环境转换(转换环境到屏幕外缓冲区,然后转换环境到帧缓冲区)。

所以当你打开 Color Offscreen-Rendered Yellow 后看到黄色,这便是一个警告,但这不一定是不好的。如果 Core Animation 能够复用屏幕外渲染的结果,这便能够提升性能。

如果你使用 layer 的方式会通过屏幕外渲染,你最好摆脱这种方式。为 layer 使用蒙板或者设置圆角半径会造成屏幕外渲染,产生阴影也会如此。

CoreAnimation OpenGL ES

Core Animation 的核心是 OpenGL ES 的一个抽象物,简而言之,它让你直接使用 OpenGL ES 的功能,却不需要处理 OpenGL ES 做的复杂的事情.

Core Animation 的 layer 可以有子 layer,所以最终你得到的是一个图层树。Core Animation 所需要做的最繁重的任务便是判断出哪些图层需要被(重新)绘制,而 OpenGL ES 需要做的便是将图层合并、显示到屏幕上。

Core Animation 通过 Core Graphics 的一端和 OpenGL ES 的另一端,精心策划基于 CPU 的位图绘制。因为 Core Animation 处在渲染过程中的重要位置上,所以你如何使用 Core Animation 将会对性能产生极大的影响。

CPU VS 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方面了.

Core Graphics / Quartz 2D

当你的程序进行位图绘制时,不管使用哪种方式,都是基于 Quartz 2D 的。也就是说,CPU 部分实现的绘制是通过 Quartz 2D 实现的。尽管 Quartz 可以做其它的事情,但是我们这里还是集中于位图绘制,在缓冲区(一块内存)绘制位图会包括 RGBA 数据。

比如,我们要绘制一个八角形,

UIKit 代码:

UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(16.72, 7.22)];
[path addLineToPoint:CGPointMake(3.29, 20.83)];
[path addLineToPoint:CGPointMake(0.4, 18.05)];
[path addLineToPoint:CGPointMake(18.8, -0.47)];
[path addLineToPoint:CGPointMake(37.21, 18.05)];
[path addLineToPoint:CGPointMake(34.31, 20.83)];
[path addLineToPoint:CGPointMake(20.88, 7.22)];
[path addLineToPoint:CGPointMake(20.88, 42.18)];
[path addLineToPoint:CGPointMake(16.72, 42.18)];
[path addLineToPoint:CGPointMake(16.72, 7.22)];
[path closePath];
path.lineWidth = 1;
[[UIColor redColor] setStroke];
[path stroke];

Core Graphics 代码:

CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, 16.72, 7.22);
CGContextAddLineToPoint(ctx, 3.29, 20.83);
CGContextAddLineToPoint(ctx, 0.4, 18.05);
CGContextAddLineToPoint(ctx, 18.8, -0.47);
CGContextAddLineToPoint(ctx, 37.21, 18.05);
CGContextAddLineToPoint(ctx, 34.31, 20.83);
CGContextAddLineToPoint(ctx, 20.88, 7.22);
CGContextAddLineToPoint(ctx, 20.88, 42.18);
CGContextAddLineToPoint(ctx, 16.72, 42.18);
CGContextAddLineToPoint(ctx, 16.72, 7.22);
CGContextClosePath(ctx);
CGContextSetLineWidth(ctx, 1);
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextStrokePath(ctx);

问题: 这个会绘制到哪儿去呢? CGContext. ctx参数正是在这个上下文中,这个上下文定义了我们需要绘制的地方。

如果我们实现了 CALayer 的 -drawInContext: 这时已经传过来一个上下文。绘制到这个上下文中的内容将会被绘制到图层的备份区(图层的缓冲区).但是我们也可以创建我们自己的上下文,叫做基于位图的上下文,比如 CGBitmapContextCreate().这个方法返回一个我们可以传给 CGContext 方法来绘制的上下文。

注意 UIKit 版本的代码为何不传入一个上下文参数到方法中?这是因为当使用 UIKit 或者 AppKit 时,上下文是唯一的。UIkit 维护着一个上下文堆栈,UIKit 方法总是绘制到最顶层的上下文中。你可以使用 UIGraphicsGetCurrentContext() 来得到最顶层的上下文。你可以使用 UIGraphicsPushContext() 和 UIGraphicsPopContext() 在 UIKit 的堆栈中推进或取出上下文。

最为突出的是,UIKit 使用 UIGraphicsBeginImageContextWithOptions() 和 UIGraphicsEndImageContext() 方便的创建类似于 CGBitmapContextCreate() 的位图上下文。混合调用 UIKit 和 Core Graphics 非常简单:

UIGraphicsBeginImageContextWithOptions(CGSizeMake(45, 45), YES, 2);
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, 16.72, 7.22);
CGContextAddLineToPoint(ctx, 3.29, 20.83);
...
CGContextStrokePath(ctx);
UIGraphicsEndImageContext();

或者另外一种方法:

CGContextRef ctx = CGBitmapContextCreate(NULL, 90, 90, 8, 90 * 4, space, bitmapInfo);
CGContextScaleCTM(ctx, 0.5, 0.5);
UIGraphicsPushContext(ctx);
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(16.72, 7.22)];
[path addLineToPoint:CGPointMake(3.29, 20.83)];
...
[path stroke];
UIGraphicsPopContext(ctx);
CGContextRelease(ctx);

图片格式

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 将不会这么高效。压缩效率将会变得低下,你甚至能看出来图片已经压缩变形了。

PNG

和 JPEG 相反,它的压缩对格式是无损的。当你将一张图片保存为 PNG,并且打开它(或解压),所有的像素数据会和最初一模一样,因为这个限制,PNG 不能像 JPEG 一样压缩图片,但是对于像程序中的原图(如buttons,icons),它工作的非常好。更重要的是,解码 PNG 数据比解码 JPEG 简单的多。

当你在你的程序中使用图片时,你需要坚持这两种格式: JPEG 或者 PNG。读写这种格式文件的压缩和解压文件能表现出很高的性能,另外,还支持并行操作。同时 Apple 正在改进解压缩并可能出现在将来的新操作系统中,届时你将会得到持续的性能提升。如果尝试使用另一种格式,你需要注意到,这可能对你程序的性能会产生影响,同时可能会打开安全漏洞,经常,图像解压缩算法是黑客最喜欢的攻击目标。

让我们再强调一遍,如果你可以,你需要为原图设置 resizable images。你的文件将变得更小,因此你只需要从文件系统装载更少的数据。

CALayer

每一个在 UIKit 中的 view 都有它自己的 CALayer。依次,这些图层都有一个叫像素位图的后备存储,有点像一个图像。这个后备存储正是被渲染到显示器上的。

通常,当你使用 CALayer 时,你会设置它的内容为一个图片。这到底做了什么?这样做会告诉 Core Animation 使用图片的位图数据作为纹理。如果这个图片(JPEG或PNG)被压缩了,Core Animation 将会这个图片解压缩,然后上传像素数据到 GPU。

尽管还有很多其他中图层,如果你是用一个简单的没有设置上下文的 CALayer,并为这个 CALayer 设置一个背景颜色,Core Animation 并不会上传任何数据到 GPU,但却能够不用任何像素数据而在 GPU 上完成所有的工作,类似的,对于渐变的图层,GPU 是能创建渐变的,而且不需要 CPU 做任何工作,并且不需要上传任何数据到 GPU。

如果一个 CALayer 的子类实现了 -drawInContext: 或者它的代理,类似于 -drawLayer:inContest:, Core Animation 将会为这个图层申请一个后备存储,用来保存那些方法绘制进来的位图。那些方法内的代码将会运行在 CPU 上,结果将会被上传到 GPU。

形状和文本图层还是有些不同的。开始时,Core Animation 为这些图层申请一个后备存储来保存那些需要为上下文生成的位图数据。然后 Core Animation 会讲这些图形或文本绘制到后备存储上。这在概念上非常类似于,当你实现 -drawInContext: 方法,然后在方法内绘制形状或文本,他们的性能也很接近。

在某种程度上,当你需要改变形状或者文本图层时,这需要更新它的后备存储,Core Animation 将会重新渲染后备存储。例如,当动态改变形状图层的大小时,Core Animation 需要为动画中的每一帧重新绘制形状.

当你用一个 UIImageView 时,事情略有不同,这个视图仍然有一个 CALayer,但是图层却没有申请一个后备存储。取而代之的是使用一个 CGImageRef 作为他的内容,并且渲染服务将会把图片的数据绘制到帧的缓冲区,比如,绘制到显示屏。

在这种情况下,将不会继续重新绘制。我们只是简单的将位图数据以图片的形式传给了 UIImageView,然后 UIImageView 传给了 Core Animation,然后轮流传给渲染服务。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值