CoreAnimation

简介

Core Animation 是一个复合引擎,它的职责就是尽可能快地组合屏幕上不同的可视内容,这个内容是被分解成独立的图层,存储在一个叫做图层树的体系之中。于是这个树形成了UIKit以及在iOS应用程序当中你所能在屏幕上看见的一切的基础。

图层与视图

在iOS当中,所有的视图都从一个叫做UIVIew的派生而来,UIView可以处理触摸事件和一些简单的动画。而 CALayer 是继承自 NSObject 的类。它们两个是一种平行的关系。

CALayer 与 UIVIew 类似。同样被层级关系树管理,也可以做动画,但是和 UIView 最大的不同是 CALayer 不能处理用户的交互。CALayer 不清楚具体的响应链,其不能够响应时间,即使它提供了一些方法来判断是否一个触点在图层的范围之内。每一个 UIVIew 都有一个 CALayer 实例的图层属性。UIVIew 来管理这些图层,确保在子视图中添加或者被移除的时候,它们关联的图层也同样对应在层级关系树当中有相同的操作。

实际上 CALayer 才是真正在屏幕上显示和做动画,UIVIew 仅仅是对其的封装,只用来处理触摸的具体功能,封装 Core Animation 底层方法的高级接口。

分离 UIVIew 与 CALayer 的设计初衷在于,这样做能够很好的进行职责分离。避免重复代码,因为在 iOS 和 macOS 两个平台用户交互方式不同,UIKit 当中的 UIVIew 和 macOS 平台的 NSView 功能相似,但是实现上有很大的区别,与Core Animation分离能够在两个平台代码共享。

这么看来使用 UIVIew 就好了,没有必要使用 CALayer 了,但是 CALayer 可以实现 UIVIew 额外的功能,比如:

  • 阴影,圆角,带颜色的边框
  • 3D变换
  • 非矩形范围
  • 透明遮罩
  • 多级非线性动画

可以这样添加 CALayer:

1
2
3
4
5
6
//create sublayer
CALayer *blueLayer = [CALayer layer];
blueLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
blueLayer.backgroundColor = [UIColor blueColor].CGColor;
//add it to our view
[self.layerView.layer addSublayer:blueLayer];

宿主图

CALayer 通过 contents 属性,还可以指定一张图片:

1
2
3
UIImage *image = [UIImage imageNamed:@"Snowman.png"];
// 这里需要将 CGImageRef 这个 Core Foundation 桥接到 Cocoa 对象中
self.layerView.layer.contents = (__bridge id)image.CGImage;

将图片设置在 CALayer 上可能会导致图片变形,这在 UIImageView 遇到的情况是一样的。可以设置 layerView 的 contentMode 属性。对 UIView 操作其实相当于对 CALayer 操作,它其实与 CALayer 的 contentsGravity 属性等价:

1
2
3
4
view.contentMode = UIViewContentModeScaleAspectFit;

// 等同于设置 CALayer 的 contentsGravity,值为一个字符串,可以去 CALayer 类中查找
self.layerView.layer.contentsGravity = kCAGravityResizeAspect;

contentsScale:属性定义了寄宿图的像素尺寸和视图大小的比例,默认情况下它是一个值为1.0的浮点数。它其实是属于支持 Retina 屏幕机制的一部分,用来判断绘制图层的时候应该为宿主图片创建的空间大小,和需要显示的图片的拉伸度(假设并没有设置contentsGravity属性)。如果 contentsScale 设置为1.0,将会以每个点1个像素绘制图片,如果设置为2.0,则会以每个点2个像素绘制图片,这就是我们熟知的Retina屏幕。所以在用代码方式设置宿主图片的时候,一定要手动设置 CALayer 的 contentsScale 属性,否则 Retina 屏幕就显示不正确:

1
layer.contentsScale = [UIScreen mainScreen].scale;

默认情况下 UIVIew 会绘制超过视图边界的内容或者子视图,CALayer 也是如此。UIVIew 中有个 clipsToBounds 属性,可以决定是否显示超出边界的内容。对应 CALayer 中的 maskToBounds。将其设置为 YES,即可裁剪掉超出宿主视图的内容。

contentsRect:属性允许我们在图层边框里显示寄宿图的一个子区域,这里与 framebounds 不同,contentsRect 不是按照点来计算的,它使用了单位坐标,是一个相对值。单位坐标比像素点更加方便度量。当大小改变的时候,也不需要再次调整。单位坐标在OpenGL这种纹理坐标系统中用得很多,Core Animation中也用到了单位坐标。默认的contentsRect是{0, 0, 1, 1},这意味着整个寄宿图默认都是可见的,如果我们指定一个小一点的矩形,图片就会被裁剪。

contentsCenter:它定义了一个固定的边框和一个在图层上可拉伸的区域。 改变contentsCenter的值并不会影响到寄宿图的显示,除非这个图层的大小改变了,你才看得到效果。和 UIImage 中的 resizableImageWithCapInsets: 方法类似。也可以在属性面板中配置 Stretching 属性。

如果 UIView 检测到 UIView 的 drawRect: 方法被实现,就会为视图分配一个宿主图。注意这个方法会造成 CPU 资源浪费。当视图出现的时候,该方法会被自动调用,然后其内容就会被缓存起来直到它需要被更新(通常是手动调用 setNeedsDisplay )。尽管影响到表现效果的属性值被更改时,一些视图类型会被自动重绘,如bounds属性)。虽然 drawRect:是 UIView 的方法,其实绘制是由 CALayer 来完成的。CALayer 有 delegate 属性,实现了 CALayerDelegate 协议,当CALayer需要一个内容特定的信息时,就会从协议中请求。在使用 CALayerDelegate 的时候,并没有对超出边界的视图提供绘制支持。

当视图被重绘,CALayer会请求它的代理给他一个寄宿图来显示。

1
2
3
4
5
6
7
8
/* If defined, called by the default implementation of the -display
* method, in which case it should implement the entire display
* process (typically by setting the `contents' property). */
- (void)displayLayer:(CALayer *)layer;

/* If defined, called by the default implementation of -drawInContext: */
// 如果代理不实现 displayLayer: 方法,就会调用 下面这个方法
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;

下面是个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];

CALayer *blueLayer = [CALayer layer];
blueLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
blueLayer.backgroundColor = [UIColor blueColor].CGColor;

//set controller as layer delegate
blueLayer.delegate = self;

//ensure that layer backing image uses correct scale
blueLayer.contentsScale = [UIScreen mainScreen].scale; //add layer to our view
[self.layerView.layer addSublayer:blueLayer];

// CALayer 不会自动重新绘制,需要手动调用该方法。
[blueLayer display];
}

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
//draw a thick red circle
CGContextSetLineWidth(ctx, 10.0f);
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextStrokeEllipseInRect(ctx, layer.bounds);
}
@end

专用图层

  • CAReplicatorLayer:为了高效生成许多相似的图层。它会绘制一个或多个图层的子图层,并在每个复制体上应用不同的变换。还可以生成镜像一样的反射效果
  • CAScrollLayer:实现图层滑动,类似 UIScrollView
  • CATiledLayer:为载入大图造成的性能问题提供了一个解决方案:将大图分解成小片然后将他们单独按需载入。
  • AVPlayerLayer:用来在iOS上播放视频的。他是高级接口例如MPMoivePlayer的底层实现,提供了显示视频的底层控制。
  • CAEmitterLayer:iOS5 中引入的高性能粒子引擎,其中粒子效果是由 CAEmitterCell 来定义。
  • CAEAGLLayer:当iOS要处理高性能图形绘制,必要时就是OpenGL。

显示动画

  • CABasicAnimation
  • CAKeyframeAnimation
  • CAAnimationGroup
  • CATransition

缓冲

Core Animation使用缓冲来使动画移动更平滑更自然,而不是看起来的那种机械。

CAMediaTimingFunction:

kCAMediaTimingFunctionLinear :线性步调对于那些立即加速并且保持匀速到达终点的场景会有意义(例如射出枪膛的子弹)
kCAMediaTimingFunctionEaseIn :一个慢慢加速然后突然停止的方法。对于之前提到的自由落体的例子来说很适合,或者比如对准一个目标的导弹的发射。
kCAMediaTimingFunctionEaseOut :它以一个全速开始,然后慢慢减速停止。它有一个削弱的效果,应用的场景比如一扇门慢慢地关上,而不是砰地一声。
kCAMediaTimingFunctionEaseInEaseOut:一个慢慢加速然后再慢慢减速的过程。这是现实世界大多数物体移动的方式,也是大多数动画来说最好的选择
kCAMediaTimingFunctionDefault:它和kCAMediaTimingFunctionEaseInEaseOut很类似,但是加速和减速的过程都稍微有些慢

用CAKeyframeAnimation来避开CAMediaTimingFunction的限制,创建完全自定义的缓冲函数。

NSTimer

iOS上的每个线程都管理了一个NSRunloop,字面上看就是通过一个循环来完成一些任务列表。但是对主线程,这些任务包含如下几项:
处理触摸事件
发送和接受网络数据包
执行使用gcd的代码
处理计时器行为
屏幕重绘

当你设置一个NSTimer,他会被插入到当前任务列表中,然后直到指定时间过去之后才会被执行。但是何时启动定时器并没有一个时间上限,而且它只会在列表中上一个任务完成之后开始执行。这通常会导致有几毫秒的延迟,但是如果上一个任务过了很久才完成就会导致延迟很长一段时间。
屏幕重绘的频率是一秒钟六十次,但是和定时器行为一样,如果列表中上一个执行了很长时间,它也会延迟。这些延迟都是一个随机值,于是就不能保证定时器精准地一秒钟执行六十次。有时候发生在屏幕重绘之后,这就会使得更新屏幕会有个延迟,看起来就是动画卡壳了。有时候定时器会在屏幕更新的时候执行两次,于是动画看起来就跳动了。
我们可以通过一些途径来优化:
我们可以用CADisplayLink让更新频率严格控制在每次屏幕刷新之后。
基于真实帧的持续时间而不是假设的更新频率来做动画。
调整动画计时器的run loop模式,这样就不会被别的事件干扰。

CADisplayLink是CoreAnimation提供的另一个类似于NSTimer的类,它总是在屏幕完成一次更新之前启动,它的接口设计的和NSTimer很类似,所以它实际上就是一个内置实现的替代,但是和timeInterval以秒为单位不同,CADisplayLink有一个整型的frameInterval属性,指定了间隔多少帧之后才执行。默认值是1,意味着每次屏幕更新之前都会执行一次。但是如果动画的代码执行起来超过了六十分之一秒,你可以指定frameInterval为2,就是说动画每隔一帧执行一次(一秒钟30帧)或者3,也就是一秒钟20次,等等。
用CADisplayLink而不是NSTimer,会保证帧率足够连续,使得动画看起来更加平滑,但即使CADisplayLink也不能保证每一帧都按计划执行,一些失去控制的离散的任务或者事件(例如资源紧张的后台程序)可能会导致动画偶尔地丢帧。当使用NSTimer的时候,一旦有机会计时器就会开启,但是CADisplayLink却不一样:如果它丢失了帧,就会直接忽略它们,然后在下一次更新的时候接着运行。

RunLoop 模式

注意到当创建CADisplayLink的时候,我们需要指定一个run loop和run loop mode,对于run loop来说,我们就使用了主线程的run loop,因为任何用户界面的更新都需要在主线程执行,但是模式的选择就并不那么清楚了,每个添加到run loop的任务都有一个指定了优先级的模式,为了保证用户界面保持平滑,iOS会提供和用户界面相关任务的优先级,而且当UI很活跃的时候的确会暂停一些别的任务。
一个典型的例子就是当是用UIScrollview滑动的时候,重绘滚动视图的内容会比别的任务优先级更高,所以标准的NSTimer和网络请求就不会启动,一些常见的run loop模式如下:
NSDefaultRunLoopMode - 标准优先级
NSRunLoopCommonModes - 高优先级
UITrackingRunLoopMode - 用于UIScrollView和别的控件的动画
在我们的例子中,我们是用了NSDefaultRunLoopMode,但是不能保证动画平滑的运行,所以就可以用NSRunLoopCommonModes来替代。但是要小心,因为如果动画在一个高帧率情况下运行,你会发现一些别的类似于定时器的任务或者类似于滑动的其他iOS动画会暂停,直到动画结束。
同样可以同时对CADisplayLink指定多个run loop模式,于是我们可以同时加入NSDefaultRunLoopMode和UITrackingRunLoopMode来保证它不会被滑动打断,也不会被其他UIKit控件动画影响性能,像这样:
self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)];
[self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:UITrackingRunLoopMode];

GPU相关的操作

大多数CALayer的属性都是用GPU来绘制。如果你设置图层背景或者边框的颜色,那么这些可以通过着色的三角板实时绘制出来。如果对一个contents属性设置一张图片,然后裁剪它。它就会被纹理的三角形绘制出来,而不需要软件层面做任何绘制。

有一些操作会降低(基于 GPU)图层的绘制:

  • 太多的几何结构:这发生在需要太多的三角板来做变换,以应对处理器的栅格化的时候。现代iOS设备的图形芯片可以处理几百万个三角板,所以在Core Animation中几何结构并不是GPU的瓶颈所在。但由于图层在显示之前通过IPC发送到渲染服务器的时候(图层实际上是由很多小物体组成的特别重量级的对象),太多的图层就会引起CPU的瓶颈。
  • 重绘:GPU的填充比率(用颜色填充像素的比率)是有限的,所以需要避免重绘(每一帧用相同的像素填充多次)的发生。
  • 离屏绘制:这发生在当不能直接在屏幕上绘制,并且必须绘制到离屏图片的上下文中的时候。离屏绘制发生在基于CPU或者是GPU的渲染,或者是为离屏图片分配额外内存,以及切换绘制上下文,这些都会降低GPU性能。对于特定图层效果的使用,比如圆角,图层遮罩,阴影或者是图层光栅化都会强制Core Animation提前渲染图层的离屏绘制。但这不意味着你需要避免使用这些效果,只是要明白这会带来性能的负面影响。
  • 过大的图片:如果视图绘制超出GPU支持的2048x2048或者4096x4096尺寸的纹理,就必须要用CPU在图层每次显示之前对图片预处理,同样也会降低性能。

CPU相关的操作:

大多数工作在Core Animation的CPU都发生在动画开始之前。这意味着它不会影响到帧率,所以很好,但是他会延迟动画开始的时间,让你的界面看起来会比较迟钝。

以下CPU的操作都会延迟动画的开始时间:

  • 布局计算:视图过于复杂,使用自动布局都会加强 CPU 的工作。
  • 视图懒加载:iOS只会当视图控制器的视图显示到屏幕上时才会加载它。这对内存使用和程序启动时间很有好处,但是当呈现到屏幕上之前,按下按钮导致的许多工作都会不能被及时响应。比如控制器从数据库中获取数据,或者视图从一个nib文件中加载,或者涉及IO的图片显示(见后续“IO相关操作”),都会比CPU正常操作慢得多。
  • Core Graphics绘制:如果对视图实现了-drawRect:方法,或者CALayerDelegate的-drawLayer:inContext:方法,那么在绘制任何东西之前都会产生一个巨大的性能开销。为了支持对图层内容的任意绘制,Core Animation必须创建一个内存中等大小的寄宿图片。然后一旦绘制结束之后,必须把图片数据通过IPC传到渲染服务器。在此基础上,Core Graphics绘制就会变得十分缓慢,所以在一个对性能十分挑剔的场景下这样做十分不好。
  • 解压图片: PNG或者JPEG压缩之后的图片文件会比同质量的位图小得多。但是在图片绘制到屏幕上之前,必须把它扩展成完整的未解压的尺寸(通常等同于图片宽 x 长 x 4个字节)。为了节省内存,iOS通常直到真正绘制的时候才去解码图片。根据你加载图片的方式,第一次对图层内容赋值的时候(直接或者间接使用UIImageView)或者把它绘制到Core Graphics中,都需要对它解压,这样的话,对于一个较大的图片,都会占用一定的时间。

当图层被成功打包,发送到渲染服务器之后,CPU仍然要做如下工作:为了显示屏幕上的图层,Core Animation必须对渲染树种的每个可见图层通过OpenGL循环转换成纹理三角板。由于GPU并不知晓Core Animation图层的任何结构,所以必须要由CPU做这些事情。这里CPU涉及的工作和图层个数成正比,所以如果在你的层级关系中有太多的图层,就会导致CPU没一帧的渲染,即使这些事情不是你的应用程序可控的。

IO相关操作

还有一项没涉及的就是IO相关工作。上下文中的IO(输入/输出)指的是例如闪存或者网络接口的硬件访问。IO比内存访问更慢,所以如果动画涉及到IO,就是一个大问题。总的来说,这就需要使用聪敏但尴尬的技术,也就是多线程,缓存和投机加载(提前加载当前不需要的资源,但是之后可能需要用到)。这些技术将会在第14章中讨论。

Core Animation 可视化的图形检测工具

Core Animation工具也提供了一系列复选框选项来帮助调试渲染瓶颈:

  • Color Blended Layers:这个选项基于渲染程度对屏幕中的混合区域进行绿到红的高亮(也就是多个半透明图层的叠加)。由于重绘的原因,混合对GPU性能会有影响,同时也是滑动或者动画帧率下降的罪魁祸首之一。
  • ColorHitsGreenandMissesRed:当使用shouldRasterizep属性的时候,耗时的图层绘制会被缓存,然后当做一个简单的扁平图片呈现。当缓存再生的时候这个选项就用红色对栅格化图层进行了高亮。如果缓存频繁再生的话,就意味着栅格化可能会有负面的性能影响了
  • Color Copied Images:有时候寄宿图片的生成意味着Core Animation被强制生成一些图片,然后发送到渲染服务器,而不是简单的指向原始指针。这个选项把这些图片渲染成蓝色。复制图片对内存和CPU使用来说都是一项非常昂贵的操作,所以应该尽可能的避免。
  • Color Immediately:通常Core Animation Instruments以每毫秒10次的频率更新图层调试颜色。对某些效果来说,这显然太慢了。这个选项就可以用来设置每帧都更新(可能会影响到渲染性能,而且会导致帧率测量不准,所以不要一直都设置它)。
  • Color Misaligned Images:这里会高亮那些被缩放或者拉伸以及没有正确对齐到像素边界的图片(也就是非整型坐标)。这些中的大多数通常都会导致图片的不正常缩放,如果把一张大图当缩略图显示,或者不正确地模糊图像,那么这个选项将会帮你识别出问题所在。
  • Color Offscreen-Rendered Yellow:这里会把那些需要离屏渲染的图层高亮成黄色。这些图层很可能需要用shadowPath或者shouldRasterize来优化。
  • Color OpenGL Fast Path Blue:这个选项会对任何直接使用OpenGL绘制的图层进行高亮。如果仅仅使用UIKit或者Core Animation的API,那么不会有任何效果。如果使用GLKView或者CAEAGLLayer,那如果不显示蓝色块的话就意味着你正在强制CPU渲染额外的纹理,而不是绘制到屏幕。
  • Flash Updated Regions:这个选项会对重绘的内容高亮成黄色(也就是任何在软件层面使用Core Graphics绘制的图层)。这种绘图的速度很慢。如果频繁发生这种情况的话,这意味着有一个隐藏的bug或者说通过增加缓存或者使用替代方案会有提升性能的空间。

    instrument 介绍

软件绘图

软件绘图不仅效率低,而且耗费内存。CALayer只需要一些与自己相关的内存:只有它的寄宿图会消耗一定的内存空间。即使直接赋给contents属性一张图片,也不需要增加额外的照片存储大小。如果相同的一张图片被多个图层作为contents属性,那么他们将会共用同一块内存,而不是复制内存块。

一旦你实现了CALayerDelegate协议中的-drawLayer:inContext:方法或者UIView中的-drawRect:方法(其实就是前者的包装方法),图层就创建了一个绘制上下文,这个上下文需要的大小的内存可从这个算式得出:图层宽图层高4字节,宽高的单位均为像素。对于一个在Retina iPad上的全屏图层来说,这个内存量就是 204815264字节,相当于12MB内存,图层每次重绘的时候都需要重新抹掉内存然后重新分配。

使用 draReact 重新绘制视图,会造成视图重新绘制,消耗大量内存。可以通过使用 CAShapeLayer 来绘制。

脏矩阵

为了减少不必要的绘制,Mac OS和iOS设备将会把屏幕区分为需要重绘的区域和不需要重绘的区域。那些需要重绘的部分被称作『脏区域』。在实际应用中,鉴于非矩形区域边界裁剪和混合的复杂性,通常会区分出包含指定视图的矩形位置,而这个位置就是『脏矩形』。

当你检测到指定视图或图层的指定部分需要被重绘,你直接调用-setNeedsDisplayInRect:来标记它,然后将影响到的矩形作为参数传入。这样就会在一次视图刷新时调用视图的-drawRect:(或图层代理的-drawLayer:inContext:方法)。

异步绘制

UIKit的单线程天性意味着寄宿图通畅要在主线程上更新,这意味着绘制会打断用户交互,甚至让整个app看起来处于无响应状态。我们对此无能为力,但是如果能避免用户等待绘制完成就好多了。

针对这个问题,有一些方法可以用到:一些情况下,我们可以推测性地提前在另外一个线程上绘制内容,然后将由此绘出的图片直接设置为图层的内容。这实现起来可能不是很方便,但是在特定情况下是可行的。Core Animation提供了一些选择:CATiledLayer和drawsAsynchronously属性。

CATiledLayer还有一个有趣的特性:在多个线程中为每个小块同时调用-drawLayer:inContext:方法。这就避免了阻塞用户交互而且能够利用多核心新片来更快地绘制。只有一个小块的CATiledLayer是实现异步更新图片视图的简单方法。

iOS 6中,苹果为CALayer引入了这个令人好奇的属性,drawsAsynchronously属性对传入-drawLayer:inContext:的CGContext进行改动,允许CGContext延缓绘制命令的执行以至于不阻塞用户交互。

它与CATiledLayer使用的异步绘制并不相同。它自己的-drawLayer:inContext:方法只会在主线程调用,但是CGContext并不等待每个绘制命令的结束。相反地,它会将命令加入队列,当方法返回时,在后台线程逐个执行真正的绘制。

根据苹果的说法。这个特性在需要频繁重绘的视图上效果最好(比如我们的绘图应用,或者诸如UITableViewCell之类的),对那些只绘制一次或很少重绘的图层内容来说没什么太大的帮助。

图像 IO

图像 IO 非常缓慢,如果图片非常小,可以在主线程中同步加载,但是对于大图来说,加载会消耗很长时间,造成滑动不流畅,滑动动画会在主线程的 run loop 中更新,所以会有更多运行在渲染服务进程中CPU相关的性能问题。

有时候在通过加载大图的时候,使用 + imageWithContentsOfFile: 会造成性能瓶颈。可以在另外一个线程当中加载图片(这并不能降低实际的加载时间,也可能会出现更糟的情况,因为系统要消耗 CPU 时间来处理加载的图片数据)。但是主线程可以有时间来响应用户输入,以及滑动动画等等。可以通过后台线程加载图片提高性能:

1
2
3
4
5
6
7
8
9
10
11
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
//load image
NSInteger index = indexPath.row;
NSString *imagePath = self.imagePaths[index];
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
//set image on main thread, but only if index still matches up
dispatch_async(dispatch_get_main_queue(), ^{
if (index == cell.tag) {
imageView.image = image; }
});
});

延迟解压

一旦图片文件被加载就必须要进行解码,解码过程是一个相当复杂的任务,需要消耗非常长的时间。解码后的图片将同样使用相当大的内存。用于加载的CPU时间相对于解码来说根据图片格式而不同。对于PNG图片来说,加载会比JPEG更长,因为文件可能更大,但是解码会相对较快,而且Xcode会把PNG图片进行解码优化之后引入工程。JPEG图片更小,加载更快,但是解压的步骤要消耗更长的时间,因为JPEG解压算法比基于zip的PNG算法更加复杂。

在加载图片的时候,iOS 会延迟解压图片的时间,直到加载到内存之后。在准备绘制图片的时候影响性能,因为需要在绘制之前进行解压(这里是消耗时间的问题所在)。可以使用 + imageNamed: 方法避免延时加载,个方法会在加载图片之后立刻进行解压。不像 + imageWithContentsOfFile: 或者其他的 UIImage 加载方法。但是 + imageNamed: 方法只对从应用资源中访问的图片有效,对于生成的图片或者是网络下载的图片就没法使用了。

可以使用 ImageIO 框架来提升性能:

1
2
3
4
5
6
7
8
NSInteger index = indexPath.row;
NSURL *imageURL = [NSURL fileURLWithPath:self.imagePaths[index]];
NSDictionary *options = @{(__bridge id)kCGImageSourceShouldCache: @YES};
CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)imageURL, NULL);
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, 0,(__bridge CFDictionaryRef)options);
UIImage *image = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);
CFRelease(source);

强制图片解压显示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
//load image
NSInteger index = indexPath.row;
NSString *imagePath = self.imagePaths[index];
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
//redraw image using device context
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, YES, 0);
[image drawInRect:imageView.bounds];
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//set image on main thread, but only if index still matches up
dispatch_async(dispatch_get_main_queue(), ^{
if (index == cell.tag) {
imageView.image = image;
}
});
});

也可以使用 CATiledLayer 用来异步加载和显示大型图片,但是它也有缺点:

  • CATiledLayer的队列和缓存算法没有暴露出来,所以我们只能祈祷它能匹配我们的需求
  • CATiledLayer需要我们每次重绘图片到CGContext中,即使它已经解压缩,而且和我们单元格尺寸一样(因此可以直接用作图层内容,而不需要重绘)。

缓存

可以使用 + imageNamed: 方法加载图片有个好处在于可以立刻解压图片而不用等到绘制的时候。这种方法仅仅适用于在应用程序资源束目录下的图片,但是大多数应用的许多图片都要从网络或者是用户的相机中获取,所以 [UIImage imageNamed:] 就没法用了。

NSCache 和 NSDictionary 类似,可以在系统低内存的时候自动丢弃存储的对象,你可以通过-setObject:forKey:和 -object:forKey: 方法分别来插入,检索。NSCache用来判断何时丢弃对象的算法并没有在文档中给出,但是你可以使用-setCountLimit:方法设置缓存大小,以及-setObject:forKey:cost:来对每个存储的对象指定消耗的值来提供一些暗示。NSCache是一个普遍的缓存解决方案,我们创建一个比传送器案例更好的自定义的缓存类。(例如,我们可以基于不同的缓存图片索引和当前中间索引来判断哪些图片需要首先被释放)。但是NSCache对我们当前的缓存需求来说已经足够了;没必要过早做优化。

图层性能

避免在内容不断变化的图层上开启图层光栅化 shouldRasterize 属性。

当图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制时,屏幕外渲染就被唤起了。屏幕外渲染并不意味着软件绘制,但是它意味着图层必须在被显示之前在一个屏幕外上下文中被渲染(不论CPU还是GPU)。图层的以下属性将会触发屏幕外绘制:

  • 圆角(当和maskToBounds一起使用时)
  • 图层蒙板
  • 阴影

大量的图层在屏幕外渲染会影响到性能,对于那些需要动画而且要在屏幕外渲染的图层来说,可以用CAShapeLayer,contentsCenter或者shadowPath来获得同样的表现而且较少地影响到性能。

cornerRadius和maskToBounds独立作用的时候都不会有太大的性能问题,但是当他俩结合在一起,就触发了屏幕外渲染。有时候你想显示圆角并沿着图层裁切子图层的时候,你可能会发现你并不需要沿着圆角裁切,这个情况下用CAShapeLayer就可以避免这个问题了。

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)viewDidLoad {
[super viewDidLoad];

//create shape layer
CAShapeLayer *blueLayer = [CAShapeLayer layer];
blueLayer.frame = CGRectMake(50, 50, 100, 100);
blueLayer.fillColor = [UIColor blueColor].CGColor;
blueLayer.path = [UIBezierPath bezierPathWithRoundedRect:
CGRectMake(0, 0, 100, 100) cornerRadius:20].CGPath;

//add it to our view
[self.layerView.layer addSublayer:blueLayer];
}

GPU每一帧可以绘制的像素有一个最大限制,是如果由于重叠图层的关系需要不停地重绘同一区域的话,掉帧就可能发生了。所以要避免混合和过度绘制:

  • 给视图设置 backgroundColor
  • 设置 opaque 为 YES

减少图层的绘制数量:

  • 通过设置对象回收, 对象回收的基础原则就是你需要创建一个相似对象池。当一个对象的指定实例(本例子中指的是图层)结束了使命,你把它添加到对象池中。每次当你需要一个实例时,你就从池中取出一个。当且仅当池中为空时再创建一个新的。
  • Core Graphics绘制。

http://ios.jobbole.com/87233/