×
广告

将像素渲染到屏幕上

96
纵横而乐
2015.07.30 10:06* 字数 9115

有很多种framework以及很多种方法的组合可以在屏幕上渲染UI元素,我们在这里讨论这个过程中发生的事情,希望这些内容可以帮助你加深对各个API性能的认识以期在决定何时以及怎样调试和修复性能问题时有所助益。以下的讨论聚焦在ios,当然大部分也适用于OS X。

以下,我们把GPU将各UI呈现到屏幕上的过程称为渲染

Graphics Stack

像素传输到屏幕上显示的过程发生了很多事情,而一旦它们出现在界面上,每个像素由3原色组成:红,绿,蓝。3种单独的颜色组分(cell)按照不同的配比组成单个具有特定颜色的像素。iPhone 5的屏幕有1136*640=727040像素,也因此有2181120个颜色组分。而15寸mac pro retina 显示屏的这个数字是15.5million。整个Graphics Stack协作以使每个组分都以正确的值显示。而在全屏模式下滚动的时候,所有这些组分都需要每秒更新60次,这个工作量十分巨大。

The Software Components

一个简化的软件栈视图看起来是这样的

ios屏幕渲染 软件栈示意图

显示屏的逻辑上层是GPU(即图形处理单元),它是一个高并行处理单元,针对并行的图形计算做了特殊的设计,使得它可以更新所有像素并将结果显示在显示屏上。其并行处理的特性使得其对不同结构(texture)的一体化合成非常地高效。简单来说,就是GPU是极度专门化的,其相对于CPU在某些处理方面更高效迅速。通用CPU的设计目的非常通用,其可以做很多事情,但对于compositing来说,GPU更出色。

GPU驱动是直接与GPU通信的代码块,不同GPU都是不同的庞然大物,驱动使得它们对下一层(通常是openGL/openGL ES)看起来都一致些。

OpenGL(open graphics library)是渲染2D/3D图形的API,GPU是专门化的硬件,openGL紧密地与GPU协作以利用GPU的功能完成硬件加速渲染。对很多人来说,openGL看起来太底层了,但其在1992年发行的时候,它是第一个标准化的与图形硬件GPU通信的方式,代表着一大步的飞跃,因为程序员用不着再为不同的GPU重写代码了。

OpenGL之上,分支稍微多了一些,在ios中,几乎所有操作都通过core animation,而在OS X中core graphics跃过core animation的现象并不罕见。一些专门化的应用比如游戏,app可能直接与open GL/openGL ES对话。更让人困惑的是,core animation又依赖core graphics做某些渲染。AVFoundation, Core Image和其他Framework混杂地使用了所有这些API。

然而需要记住的一点是:GPU非常强大的图形硬件 且其在显示中承担着中心的角色,且与CPU相连。在硬件上,两者之间存在总线,同时有OpenGL, Core Animation, and Core Graphics这些Framework精心地主导着GPU与CPU之间的数据传输。为了将你的像素显示在显示屏上,一些处理会在CPU上进行。然后数据会被传输到GPU中,然后数据接着在GPU中进行处理并最终呈现在显示屏上。

上述这个像素数据的旅程有着自己的惊险,且整个过程都伴随着无尽的权衡和妥协。

硬件选手


ios图形渲染过程中 所参与的硬件协作图

简单描述这个挑战的视图看起来是这样:GPU拥有用来合成每一帧的各结构(都是位图),每个结构都占据VRAM(video ram),也因此决定了GPU所能持有的结构的量会有一个上限。虽然GPU在合成方面极为有效,但某些种类的合成比其它种类的合成任务更复杂,同时GPU在16.7ms中能做的事情的量也是有限的。

另一个挑战是将你的数据传输到GPU,为了让GPU访问到数据,需要将其从RAM转移到VRAM,这个过程称为上传到GPU。这个过程可能看起来很细小,但对于大量的结构来说,这个过程可能会很耗时。

最终,CPU运行程序。比如执行到CPU从bundle中加载一张PNG图片并将其解压缩,这些都在CPU中进行,而如果想显示这张图片,则需要将其上传到GPU中。寻常的显示文字的任务对于需要利用core text和core graphics frameworks一起工作以从文本产生一张位图的CPU来说,是一件极其复杂的任务。一旦准备好了之后,其会上传到GPU成为一个结构(texture),等待被渲染。当你滚动或者用其他方式在屏幕上移动这段文本时,这个结构会被重用,CPU会告诉GPU这个结构的新位置在哪里,以便GPU重用此结构。CPU不需要重新呈现这块文本,其对应的位图也不需要重新上传。

Compositing

在图形世界中,compositing是用来描述将不同位图合成为最终呈现在屏幕上的整个图像的过程的术语。这个概念如此明晰,以至于很容易忽视其中所蕴含的复杂性和大量的计算。

我们先避开一些深奥的例子,假定屏幕上的所有东西都是一个结构(texture),一个结构是一块RGBA值填充的矩形。在core animation的概念中,这即是CALayer。

在这个简单的假定下,每一层都是一个texture,且所有这些结构都以某种方式层叠起来。对于每个像素,GPU需要根据这些层叠的结构计算它们混合(blend/mix)之后每个像素点正确的RGB值,这也正是compositing的工作。

如果只有一个texture,且其与屏幕一般大,并与屏幕各边对齐,则其每个像素正好与屏幕每个像素对齐,屏幕上每个像素点的值均与此texture的每个对应的像素点的RGB值相同。

如果在上述唯一的texture上覆盖第二个texture,则GPU会将这个texture合成到第一个上去,有不同的blend模式,但如果我们假定两个texture像素对齐,且我们在使用普通的blend模式,则合成的结果颜色是通过如下公式计算的:

R = S + D * (1 - Sa)

结果颜色是源颜色(top texture)加上目标色(lower texture)所被允许透出来的色值。这个原理的意思即是,如果源颜色为40%的红,目标色为红,则最终显示的即为100%的红,因为RGBA的显示原则是:将RGB部分的各个值乘以alpha通道的值,最终得到的RGB显示在屏幕上。这个公式中的所有颜色(即S,D)都假定已经预先乘过其alpha通道的值。

OK,事情,开始复杂起来了,此时我们再假定所有texture都是不透明的,亦即alpha = 1。如果目标色为蓝(0,0,1),源texture为红(1,0,0),由于Sa为1,所以结果色为 R = S,即为红色。

若源为0.5透明,则其RGB值将是(0.5,0,0),则公式将会如下:

RGBA颜色合成实例

最终的颜色为(0.5,0,0.5),这是饱满的紫红色或者说紫色。这是直观上将透明红混合到蓝色背景上将呈现的颜色。

始终应当铭记的是,我们所做的是将一个texture中的像素组合到另一个texture中的像素上。GPU需要对这两个texture所层叠区域的所有像素都做组合的处理,鉴于大多app都有很多层,所以有很多texture需要组合。这也使得即便是对GPU这样专门化的设备,也够它忙活了。

Opaque vs. Transparent

源色完全不透明时,最终颜色将与源色相同,这样,GPU就不用blend所有texture的色值,而只需要复制源texture的像素色值即可。但GPU无法确知texture是否透明,只有程序员自己知道这块CALayer的具体情况。由此,CALayer有一个称为opaque的属性,若为YES,则GPU不会做blending而会简单地从这个layer取像素,而不管位于其下的UI元素,这可以减轻GPU很多工作。Instruments的color blended layers选项即是对此种特性的丈量,其使得可以查看到哪些层是标记为透明的,亦即哪些层是GPU做过blending的。(在xcode6 中 将alpha值在1与非1之间修改时似乎也会引起blend的变化)。组合不透明层可以少做运算。

所以,如果知道layer为opaque,则确保设置opaque为YES。如果加载一张没有alpha通道的图,并将其在UIImageView中显示,则这个属性是默认设置的。但需要注意的是,没有alpha通道的图片与alpha值 为100%的图片之间的差异是巨大的。后者在core animation中处理的时候会被当成可能存在alpha值非100%的像素点来处理,即渲染的时候还是会有blend。在finder中可以在Get info中的More info部分看到图片是否有alpha通道。

Pixel Alignment and Misalignment

在layer与屏幕完美对齐的时候,每个像素都是对齐的,这时候计算会相对简单一些,这时候,GPU计算屏幕上的一个像素点的色值的时候,只需要将这点上所有层的色值合成一下即可。或者如果顶层结构是不透明的话,GPU只需要复制顶层结构的色值即可。

当layer的像素与屏幕像素一一对齐的时候称其为pixel-aligned,主要有两种原因会导致无法达到pixel-aligned,一种是结构可能会缩放,另一种原因是结构的origin可能并不位于像素边界上。这两种情况都会导致GPU做额外的工作,它不得不将像素的源色值与多个色值混合以合成最终的色值。

同样,core animation instrument和模拟器有一个color misaligned images的选项以标记CALayer的这些情况。

Masks

一个layer可以拥有与之关联的一个mask,它是一个alpha值的位图,在layer与其下方的色值合成之前会应用在其像素点本身的色值。当设置layer的cornerradius时,你其实是高效地为layer设置。也可以设置做任意的mask,比如一个形如A的mask,只有layer中属于mask的部分才会得到渲染。

Offscreen Rendering

离屏渲染可以由core animation主动触发或者由应用触发。离屏渲染会将layer树的一部分渲染到一个新的buffer中(整个过程为离屏状态,即不渲染在屏幕上),然后这个buffer再渲染到屏幕上。

在合成很耗时的时候,可能会想进行离屏幕渲染,这种方式可以缓存合成的结构/layers。如果渲染树很复杂,那么可以强制离屏渲染将layers缓存并将此份缓存供于合成到屏幕上。

如果你的app结合了很多layers且想它们一起做动画,那么GPU将不得不每1/60秒重新合成这些layers到其下的像素色值上。在使用离屏幕渲染的时候,GPU先将这些layer结合到一个基于新的结构的位图缓存中,然后将这个结构绘制到屏幕上去。那些这些layers一起移动的时候,GPU就可以重用这个位图缓存并节省计算资源。而需要注意的是,这只适用于这些layers并不会改变的情况。如果是这样的话,那么需要重新合成缓存位图。可以通过设置shouldRasterize为YES以触发这种行为(哪种行为呢,shouldRaterize属性标识在合成之前是否将calayer渲染成位图,一个有趣的stackoverflow上的参考例子)。

然而,离屏渲染也只是一种需要不断权衡的选择。原因之一是其会导致运行变慢。创建额外的离屏缓存是GPU需要做的额外操作,尤其是如果从未重用用这个位图的情况下会更糟。而如果被重用,则GPU的工作量可以得到减轻。需要测量GPU利用率和帧率以确定是否对性能有改善。

离屏渲染也可能会作为一种副作用而发生,如果直接或间接在使用mask到一个layer,core animation将不得不做离屏渲染以应用这个mask。这会对GPU增加负担。

instruments core animation工具有一个称为color offscreen-rendered yellow的选项,可以将需要离屏渲染的部分标记为黄色(模拟器中也有这个选项)。确保同时勾选Color Hits Green and Misses Red,绿色代表离屏渲染的缓存被重用了,而红色代表缓存被重新创建。

一般来说,需要避免离屏渲染,因为其代价昂贵。合成layers到帧缓存(最终会渲染到屏幕上)中是最经济的做法,相比于创建缓存,将结构渲染进其中,再将缓存渲染回帧缓存中。同时还有两个耗时的上下文切换过程:切换到离屏渲染缓存,切换回帧缓存。

所以当你看到离屏渲染的黄色的时候,这并不一定是坏事,因为如果core animation可以重用离屏渲染的缓存的话,可以改善性能。

同样需要注意的是,rasterized layers也是有容量限制的,apple的说法是大约为rasterized layers或者说离屏渲染缓存预留了两倍屏幕大小的空间。

如果你使用layers的方式会导致离屏渲染,那你最好避免全部都这样做,设置cornerradius以及shadow都会引发离屏渲染。

对于masks,若想使用cornerradius或者clipsToBounds/masksToBounds,可以通过使用已经将mask内化在内容中的UI元素,比如可以使用已经应用了mask的真实图片。当然,这也都是需要权衡的。如果你想对一个设置了contents属性的layer应用矩形mask,可以使用contentsRect而避免使用mask

如果设置shouldRasterize为YES,记得根据contentsScale设置rasterizationScale

(科普一下吧,contentsScale这个属性标识以点为标记的逻辑坐标与以像素为标记的物理坐标之间的对应关系,这个值默认是1.0,但UIView默认根据屏幕分辨率将此值设置为合适的值。对于自己创建的calayer,需要根据屏幕分辨率及所呈现的内容决定将此值设置为合适的值)

还是Compositing

对于alpha通道的合成,可以参考wiki,同时在像素部分会接着介绍Rgba在内存中的表示方式。

Core Animation & OpenGL ES

core animation使得可以在屏幕上让UI动起来,而此处我们更多侧重在绘制方面。需要指出的是,core animation可以使渲染极为高效,才使得可以在一秒内完成60帧的动画。

core animation的核心层是基于OpenGL es 的抽象,在上面的内容中我们将texture与layer当成同一个概念来运用,但其实它们不是一个东西,只是很相似而已。

core animation的layer可以有sublayer,所以layer其实是一个layer树,core animation大量的工作都是哪些layer需要绘制或重绘,以及需要调用哪些OpenGL es接口以将layers合成到屏幕上。

比如core animation会在layer的contents设置为CGImageRef时,为其创建一个OpenGL Texture,以保证此image位图会上传到GPU中对应的texture。或者如果重载drawInContext,core animation会创建一个Texture以保证调用的core graphics绘制的内容会写进textture的位图数据中。layer的属性和子类会影响OpenGL渲染的行为,且其将OpenGL ES封装到了calayer的概念体系中。

core animation精准地协调了基于CPU端的core graphics绘制与基于GPU端的绘制,由于core animation处理渲染流程的这个关键位置,使得core animation的使用可能严重影响到性能

CPU bound vs. GPU bound

绘制的过程有很多组成部分的参与,两个重要的硬件部分是CPU和GPU。

为了达到60的帧率,需要确保CPU和GPU均不会过载。即使达到了60fps,也需要将工作更多地移到GPU上。因为CPU更重要的工作是运行app而非绘制,同时也是因为GPU比CPU更擅长绘制。

鉴于CPU与GPU均会影响到绘制性能,如果已经使用了所有的GPU资源,则称此时为GPU bound,即GPU限制了绘制的性能,同样地对CPU而言称为CPU bound

检测GPU bound需要使用OpenGL ES Driver instrument, 点按i 按钮,再configure,勾选Device Utilization %。Time Profiler instrument可以分析CPU bound

Core Graphics / Quartz 2D

相对于Quartz 2D来说,大家更多是通过包含它的core graphics Framework来认识它的。

Quartz 2D的特性远比此处所能提到的多,这里不会讲到关于PDF的那很大的一部分,创建,渲染,解析或者打印PDF。实际上打印和创建PDF与渲染位图到屏幕上的过程大部分都一样,其实它们都是基于Quartz 2D的。

接下来简单介绍下Quartz 2D的主要概念,细节可以到Quartz 2D Programming Guide 。

quartz 2d是非常强大的2D绘制工具,基于路径的绘制,反锯齿渲染,透明layers,分辩率和设备无关这些特性。其非常艰深难懂,同时其基于底层的C API也让它看起来更为晦涩。

然而其主要概念却相对简单,UIKit和AppKit都使用易于使用的API封装了quartz 2d,且就算纯C API使用之后也可以掌握。quartz 2d是一个可以完成 photoshop和illustrator绝大部分操作的强大的绘制引擎。apple用股票APP作为quartz 2d使用的例子,其中的图是用quartz 2d动态渲染的。

当你的app使用位图绘制的时候 ,其实是通过各种方式使用到了quartz 2d。即CPU部分的绘制过程会使用到quartz 2d。接下来我们聚焦在quartz的位图绘制部分,即将结果绘制成RGBA数据表示的缓存。

比如我们想绘制一个八边形,使用UIKit的代码如下:

绘制八边形的UIKit代码

对应的Core Graphics代码大致是这样:

绘制八边形的core graphics代码

到这里可能会问:这个绘制是发生在哪里的呢?原来这块绘制是发生在CGContext中的,其中的ctx参数即是在这个上下文,这个上下文定义了绘制的目的地。在实现calayer的drawInContext方法时,会收到一个上下文参数,在这个上班文绘制会绘制进它的缓存中。我们也可以通过CGBitmapContextCreate创建自己的绘制上下文,即基于位图的上下文。

大家可能注意到了UIKit中的各方法并没有接收上下文这样一个参数,这是因为使用UIKit和AppKit的时候上下文是隐含的,UIKit维护了一个上下文栈,并始终在栈顶的上下文绘制。可以使用UIGraphicsGetCurrentContext获取这个上下文,也可以通过UIGraphicsPushContext() and UIGraphicsPopContext()进行上下文的入栈和出栈。

同时UIKit可以通过UIGraphicsBeginImageContextWithOptions() and UIGraphicsEndImageContext()创建与CGBitmapContextCreate方法创建的相似的位图上下文。UIKit和core graphics的混合调用可能像这样:

方法一

或者像这样

方法二

apple自诩core graphics的输出拥有难以想象的逼真效果,虽然无从得知其具体实现细节,但core graphics的图形模型与Adobe Illustrator and Adobe Photoshop的实现非常相近。它们都源于基于Display PostScript的NeXTSTEP。

CGLayer

永远不要用CGLayer

Pixels

像素点的RGB数据是怎样存储在内存中的呢,答案是很多种

Default Pixel Layouts

一个通常的存储格式是32bits-per-pixel,8bit-pre-component,且alpha值预乘,在内存中的存储格式为ARGB,即如果alpha为0.33的白色,则其存储形式为84,84,84,84。

另一种常用格式是32bpp,8bpc,alpha-none-skip-first,其与ARGB存储在内存的形式是一样的,但alpha值并没有使用,因为这种存储格式存储的是不透明的位图,名为xRGB。虽然浪费了25%的字节,但由于现代CPU及图像算法更喜欢这种32位对齐的数据,相比于3字节避免了很多的移位和mask,同时与ARGB混合使用的时候也更方便,所以这种方式也很普遍。

core graphics也支持RGBA和RGBX

像素存储格式

大多数处理位图的时候,都是在处理core graphics/quartz 2D,接着来看支持的RGB存储形式:

16bpp,5bpc 不带alpha,这种方案对存储RGB数据很适用,但由于每组分只有5位,所以图片可能产生banding artifacts

64bpp,16bpc或者128bpp,32bpc的方案,画质会更逼真,但带来更大的计算量和内存占用量。

同时core graphics还支持一些grayscale和CMYK格式,以及只有alpha值的格式(用于mask)

Planar Data

一些framework会将所有像素的RGB分别存储在3块区域中,即每场区域分别存储所有像素的红,绿,蓝组分的值,这种分别存储这3种独立组分的方式为planar components, or component planes。一些video framework会在某种情况下使用这种方式。

YCbCr

YCbCr是在视频领域更常见的一种格式。其也包含三个组成部分,但它更接近人眼感知色彩的方式。人眼视觉对两个色度组分Cb和Cr的逼真程序并不敏感,但却对亮度很敏感。于是在数据压缩的时候,在同一个感知质量的前提下可以尽量多压缩cb和Cr部分。JPEG有时候也因为同样的原因会将像素数据从RGB转为YCbCr。

图像格式

大多数人都对JPEG有一个误解,即它只是存储像素RGB数据的一种格式而已,而事实远非如此。将JPEG转换为像素数据是一个非常复杂的过程,肯定不是一个周末能完成的。对每个颜色域,JPEG压缩使用基于离散余弦变幻的算法将空间信息转换为频域信息来进行压缩。这个信息再经过量化,顺序化再使用哈夫曼编码打包。而且通常数据会先从RGB转换到YCbCr。解压JPEG需要倒序把这些都解一遍。也正是因为如此,由JPEG创建UIImage并将其显示在屏幕上会有延迟,因为CPU忙着解压,如果每个tablecell都对应一张JPEG,则table的滚动会很不流畅。

那么为什么要用JPEG呢,因为JPEG压缩图片是一流的,未压缩的iphone 5照片可以达到24M,而在默认的压缩设定下,camera roll中的照片通常是2到3M。JPEG是有损压缩,它会将人类不太好感知到的信息都丢弃掉,且比通常的压缩算法比如gzip都能达到更大的压缩率。但它只对照片有效,因为照片中有很多人类视觉感知不到的东西。如果截取网页的图像,JPEG并不能很好地压缩这类满是文字的图片,压缩率会变小,且压缩效果可能是图片内容被明显地改变了。

PNG

PNG发音为ping,与JPEG相反的是,它是无损压缩格式。图片存储为PNG之后,重新打开仍然和原图片是一样的,由于是无损压缩,所以它达不到JPEG的压缩率,但对于按钮图标等,它的压缩率是很好的。且解压PNG图片比JPEG更简单。PNG支持带alpha和不带alpha的RGB。

图片格式的抉择

在app中使用照片时,可以从JPEG和PNG中选择,因为其解压和压缩器均经过了优化并在一定程度上支持并行。如果使用其他的格式,那么一来性能可能不会达到使用这两种格式的高度,而且可能会遭遇安全漏洞,因为图片解压器是攻击的很常见的方式。

Xcode对PNG的优化不同于大多数优化引擎,经xcode优化过的PNG图片严格意义上来说已经不再是PNG图片了,但ios可以读取,且解压更快。这其中主要需要提及的是对像素存储结构进行了更改,因为有很多种表示 RGB的格式,如果格式并不是ios 图片系统所需要的,会对每个像素进行数据的修改。需要强调的是,如果可以的话尽量用resizable images,因此需要从文件系统中加载的数据会更少,解压工作量也更少。

UIKit和像素

UIKit中每个view都有自己的calayer,这个layer有一个位图的缓存,它会最终绘制到屏幕上。

drawRect

如果view重载了drawRect,则调用 setNeedsDisplay的时候,则UIKit会在layer上调用setNeedsDisplay,这会将layer的某个flag设置为dirty代表需要显示。

接下来当绘制系统ready的时候,会调用view的layer的display方法,这个时候layer建立它的缓存,然后创建core graphics上下文即CGContextRef,即可使用它来绘制内容到layer上。如果在drawRect中使用UIRectFill或者[UIBezierPath fill],它们会使用这个上下文,因为此时这个layer的上下文已经被push到当前图形上下文栈顶了。

自此layers缓存会不断被绘制到屏幕上,直到view的setNeedsDisplay再次被调用,layer缓存被更新。

没有drawRect

如果没有重载drawRect,情况会不太一样。比如UIImageView,它仍有calayer,但并不会分配缓存,相反,它使用一个cgimageref作为content,绘制server会将这个image的位绘制到帧缓存中,亦即屏幕上。

在这种情况下,没有发生绘制,我们只是将位图数据以image的形式传递给了uiimageview,它再将图片位图传递给Core animation,并最终传递到绘制Server中。

To -drawRect: or Not to -drawRect:

最快的绘制是不绘制,大多数时候,可以通过views或者layers的组合来合成自定义的view。可以从自定义控件中了解更多,上述方法是推荐的做法,因为uikit中的views 已经极度优化过了。

需要自定义绘制代码的场合比如WWDC 2012's session 506 Optimizing 2D Graphics and Animation Performance中展示的finger painting app。

另一个是ios股市app,它使用core graphics绘制而成。但说到自定义绘制,并不一定要重载drawRect,有时使用UIGraphicsBeginImageContextWithOptions() or CGBitmapContextCreate() 创建位图并将其设置为calayer 的contents会更有效。下面我们会给出一个例子。

关于绘制色块的例子

- (void)drawRect:(CGRect)rect

{

[[UIColor redColor] setFill];

UIRectFill([self bounds]);

}

这段 代码根据上述的讨论,是不合理的。因为会导致core animation创建一块缓存,并由core graphics将红色填充进这块缓存,然后这块缓存被上传到GPU。

其实可以不重载drawRect,而是设置layer的backgroundColor,而如果layer为CAGradientLayer,同样适用。

关于可变大小的图像

使用小图片扩展成需要的大小可以有效地减少占用的显存,core animation可以使用CALayer的contentsCenter属性resize图像。但大多数情况下,你可能会想用[UIImage resizableImageWithCapInsets:resizingMode:]

Concurrent Drawing

issue2是关于concurrency,UIKit的线程模型很简单,但只可以在主线程中使用UIKit类,和并发绘制有什么关系呢:

如果需要重载drawRect,所需要的绘制会比较耗时。而为了追求顺畅的动画效果,不会希望在主线程队列中做这份操作。虽然并发并不简单,但并发绘制还是可以实现的。

在主线程之外是不可以向CALayer的缓存中写入任何东西的,但可以在向disconnected位图缓存中写入。

和core graphics部分提到的那样,所有core graphics绘制都需要在一个上下文中进行,同时UIKit都有一个当前上下文的概念,而current context是相对于每个线程的。

下面我们将在另一个线程队列中创建一个image之后,返回主线程将其设置到UIImageView中。这个技巧在2012 wwdc session211中讨论过。

需要保证的是这个方法调用的绘制代码都必须是线程安全的,比如访问到的属性等,因为调用是从另一个线程发起的,如果这个方法在你自己的view类中,会更复杂一些。另一个选项是,也可能更简单的选项是在另一个单独的渲染器类中设置所有需要的属性并触发它去创建这个image。如果这么做的话,可以选择UIImageView或者UITableViewCell。

所有UIKit绘制API在其它线程中都是安全的,只要确保在同一个操作中,调用配对的UIGraphicsBeginImageContextWithOptions()和UIGraphicsEndIamgeContext()即可。

由于并发固有的复杂性,还需要实现取消后台渲染。同时需要在渲染队列中设置一个合理的最大并发操作数。要满足这些要求,需要NSOperation中实现renderInImageOfSize。

最后需要指出的是,异步设置UITableViewCell的content是会有隐患的。因为它可重用的特性使得异步绘制完成的时候,它可能已经用于其它地方了。

以CALayer的古怪之处作结吧

现在可以说CALayer和GPU texture有关联且很相似,layer有一个缓存最终绘制到屏幕上。而通常的使用经常是CALayer的content被设置成一个图像,这样的话,这个图像位图即成为CALayer对应的texture,core animation会将图像解压并将其上传给GPU

当然还有其它各类的layer,如果使用纯CALayer,不要设置contents,可以设置background color, 这样core animation不需要上传任何数据到GPU,GPU会做好一切。对于gradient layers也一样,GPU可以创建gradient,不需要CPU工作,也不需要上传数据到GPU。

如果重载了CALayer的drawInContext或者它的delegate,对应的drawLayer:inContext:,core animation会创建一块缓存用于存放在CPU绘制的内容并上传到GPU中。

Shape and Text Layers

core animation会为这些内容单独分配一块缓存以存储位图数据,类似于drawInContext中绘制的概念。在你改变形状或者文本的时候,core animation会更新缓存并重新渲染。

异步绘制

CALayer有一个drawAsynchronously的方法,看起来可以解决所有总是,但它的性能可能好,也可以很不好。当设置为YES的时候,drawRect: / -drawInContext:方法仍会在主线程调用,但所引起的所有对core graphics(uikit的调用也会最终转化为对core graphics的调用)的调用都不会做任何绘制,而是被延迟并在后台线程进行。

所以其实绘制命令是先被记录下来并接下来在其他线程中执行,所以需要做更多的工作,更多的内存,但工作都从主线程中移走了,所以需要测试才看是否适合特定的需求。

这个方法对重绘制的功能可能会起到改进性能的作用,并不太适合轻绘制的功能。



question:

1 the texture’s origin is not at a pixel boundary 是什么意思

2 calayer和uiview这对欢喜冤家各自的适用范围在哪里呢


附注更精彩

根据ios渲染UI中提及的渲染机制,mainrunloop的主要机制是在睡眠之前去进行CATransaction的提交,同时为了及时处理用户交互事件,需要尽可能快地睡眠,由此涉及到的界面刷新机制即是经Core Animation commit后的layer tree才会引发界面更新,而commit是在main RunLoop的beforewaiting事件响应并进行的,所以界面刷新的机制即演变成了怎样在需要刷新的时候唤醒runloop的问题了。

界面刷新的时机发生在睡眠之前,而被唤醒的原因有很多,比如定时器timeout,硬件事件触发,网络socket触发等,它们都能够触发runloop唤醒,界面触发commit,即可以保证界面在一切必要的时刻被刷新。

正常情况下,即runloop的defaultmode下,除非发生animation,界面一般不会刷新也不可能达到60fps,某次runloop唤醒并触发animation之后,render server通过cadisplaylink触发定期唤醒runloop以提交动画各帧。

而runloop的uitrackingmode即scrollview的滚动时期,比如典型的uitableview和collectionview的滚动时,这时候也会触发cadisplaylink定时唤醒runloop。

objc.io
Web note ad 1