要更快性能,也要作对正确的事情。 ——Stephen R. Coveygit
在第14章『图像IO』讨论如何高效地载入和显示图像,经过视图来避免可能引发动画帧率降低的性能问题。在最后一章,咱们将着重图层树自己,以发掘最好的性能。github
寄宿图能够经过Core Graphics直接绘制,也能够直接载入一个图片文件并赋值给contents
属性,或事先绘制一个屏幕以外的CGContext
上下文。在以前的两章中咱们讨论了这些场景下的优化。可是除了常见的显式建立寄宿图,你也能够经过如下三种方式建立隐式的:1,使用特性的图层属性。2,特定的视图。3,特定的图层子类。缓存
了解这个状况为何发生什么时候发生是很重要的,它可以让你避免引入没必要要的软件绘制行为。ide
CATextLayer
和UILabel
都是直接将文本绘制在图层的寄宿图中。事实上这两种方式用了彻底不一样的渲染方式:在iOS 6及以前,UILabel
用WebKit的HTML渲染引擎来绘制文本,而CATextLayer
用的是Core Text.后者渲染更迅速,因此在全部须要绘制大量文本的情形下都优先使用它吧。可是这两种方法都用了软件的方式绘制,所以他们实际上要比硬件加速合成方式要慢。布局
不论如何,尽量地避免改变那些包含文本的视图的frame,由于这样作的话文本就须要重绘。例如,若是你想在图层的角落里显示一段静态的文本,可是这个图层常常改动,你就应该把文本放在一个子图层中。性能
在第四章『视觉效果』中咱们提到了CALayer
的shouldRasterize
属性,它能够解决重叠透明图层的混合失灵问题。一样在第12章『速度的曲调』中,它也是做为绘制复杂图层树结构的优化方法。学习
启用 shouldRasterize 属性会将图层绘制到一个屏幕以外的图像。而后这个图像将会被缓存起来并绘制到实际图层的contents
和子图层。若是有不少的子图层或者有复杂的效果应用,这样作就会比重绘全部事务的全部帧划得来得多。可是光栅化原始图像须要时间,并且还会消耗额外的内存。优化
当咱们使用得当时,光栅化能够提供很大的性能优点(如你在第12章所见),可是必定要避免做用在内容不断变更的图层上,不然它缓存方面的好处就会消失,并且会让性能变的更糟。动画
为了检测你是否正确地使用了光栅化方式,用Instrument查看一下Color Hits Green和Misses Red项目,是否已光栅化图像被频繁地刷新(这样就说明图层并非光栅化的好选择,或则你无心间触发了没必要要的改变致使了重绘行为)。ui
Offscreen rendering does not necessarily imply software drawing, but it means that the layer must first be rendered (either by the CPU or GPU) into an offscreen context before being displayed. The layer attributes that trigger offscreen rendering are as follows:
当图层属性的混合体被指定为在未预合成以前不能直接在屏幕中绘制时,屏幕外渲染就被唤起了。屏幕外渲染并不意味着软件绘制,可是它意味着图层必须在被显示以前在一个屏幕外上下文中被渲染(不论CPU仍是GPU)。图层的如下属性将会触发屏幕外绘制:
圆角(当和maskToBounds
一块儿使用时)
图层蒙板
阴影
屏幕外渲染和咱们启用光栅化时类似,除了它并无像光栅化图层那么消耗大,子图层并无被影响到,并且结果也没有被缓存,因此不会有长期的内存占用。可是,若是太多图层在屏幕外渲染依然会影响到性能。
有时候咱们能够把那些须要屏幕外绘制的图层开启光栅化以做为一个优化方式,前提是这些图层并不会被频繁地重绘。
对于那些须要动画并且要在屏幕外渲染的图层来讲,你能够用CAShapeLayer
,contentsCenter
或者shadowPath
来得到一样的表现并且较少地影响到性能。
cornerRadius
和maskToBounds
独立做用的时候都不会有太大的性能问题,可是当他俩结合在一块儿,就触发了屏幕外渲染。有时候你想显示圆角并沿着图层裁切子图层的时候,你可能会发现你并不须要沿着圆角裁切,这个状况下用CAShapeLayer
就能够避免这个问题了。
你想要的只是圆角且沿着矩形边界裁切,同时还不但愿引发性能问题。其实你能够用现成的UIBezierPath
的构造器 +bezierPathWithRoundedRect:cornerRadius: (见清单15.1).这样作并不会比直接用cornerRadius
更快,可是它避免了性能问题。
清单15.1 用CAShapeLayer
画一个圆角矩形
#import "ViewController.h"#import <QuartzCore/QuartzCore.h>@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *layerView;@end@implementation ViewController- (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]; }@end
另外一个建立圆角矩形的方法就是用一个圆形内容图片并结合第二章『寄宿图』提到的 contentsCenter 属性去建立一个可伸缩图片(见清单15.2).理论上来讲,这个应该比用CAShapeLayer
要快,由于一个可拉伸图片只须要18个三角形(一个图片是由一个3*3网格渲染而成),然而,许多都须要渲染成一个顺滑的曲线。在实际应用上,两者并无太大的区别。
清单15.2 用可伸缩图片绘制圆角矩形
@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; //create layer CALayer *blueLayer = [CALayer layer]; blueLayer.frame = CGRectMake(50, 50, 100, 100); blueLayer.contentsCenter = CGRectMake(0.5, 0.5, 0.0, 0.0); blueLayer.contentsScale = [UIScreen mainScreen].scale; blueLayer.contents = (__bridge id)[UIImage imageNamed:@"Circle.png"].CGImage; //add it to our view [self.layerView.layer addSublayer:blueLayer]; }@end
使用可伸缩图片的优点在于它能够绘制成任意边框效果而不须要额外的性能消耗。举个例子,可伸缩图片甚至还能够显示出矩形阴影的效果。
在第2章咱们有提到shadowPath
属性。若是图层是一个简单几何图形如矩形或者圆角矩形(假设不包含任何透明部分或者子图层),建立出一个对应形状的阴影路径就比较容易,并且Core Animation绘制这个阴影也至关简单,避免了屏幕外的图层部分的预排版需求。这对性能来讲颇有帮助。
若是你的图层是一个更复杂的图形,生成正确的阴影路径可能就比较难了,这样子的话你能够考虑用绘图软件预先生成一个阴影背景图。
在第12章有提到,GPU每一帧能够绘制的像素有一个最大限制(就是所谓的fill rate),这个状况下能够轻易地绘制整个屏幕的全部像素。可是若是因为重叠图层的关系须要不停地重绘同一区域的话,掉帧就可能发生了。
GPU会放弃绘制那些彻底被其余图层遮挡的像素,可是要计算出一个图层是否被遮挡也是至关复杂而且会消耗处理器资源。一样,合并不一样图层的透明重叠像素(即混合)消耗的资源也是至关客观的。因此为了加速处理进程,不到必须时刻不要使用透明图层。任何状况下,你应该这样作:
给视图的backgroundColor
属性设置一个固定的,不透明的颜色
设置opaque
属性为YES
这样作减小了混合行为(由于编译器知道在图层以后的东西都不会对最终的像素颜色产生影响)而且计算获得了加速,避免了过分绘制行为由于Core Animation能够舍弃全部被彻底遮盖住的图层,而不用每一个像素都去计算一遍。
若是用到了图像,尽可能避免透明除非很是必要。若是图像要显示在一个固定的背景颜色或是固定的背景图以前,你不必相对前景移动,你只须要预填充背景图片就能够避免运行时混色了。
若是是文本的话,一个白色背景的UILabel
(或者其余颜色)会比透明背景要更高效。
最后,明智地使用shouldRasterize
属性,能够将一个固定的图层体系折叠成单张图片,这样就不须要每一帧从新合成了,也就不会有由于子图层之间的混合和过分绘制的性能问题了。
初始化图层,处理图层,打包经过IPC发给渲染引擎,转化成OpenGL几何图形,这些是一个图层的大体资源开销。事实上,一次性可以在屏幕上显示的最大图层数量也是有限的。
确切的限制数量取决于iOS设备,图层类型,图层内容和属性等。可是总得说来能够容纳上百或上千个,下面咱们将演示即便图层自己并无作什么也会遇到的性能问题。
在对图层作任何优化以前,你须要肯定你不是在建立一些不可见的图层,图层在如下几种状况下回事不可见的:
图层在屏幕边界以外,或是在父图层边界以外。
彻底在一个不透明图层以后。
彻底透明
Core Animation很是擅长处理对视觉效果无心义的图层。可是常常性地,你本身的代码会比Core Animation更早地想知道一个图层是不是有用的。理想情况下,在图层对象在建立以前就想知道,以免建立和配置没必要要图层的额外工做。
举个例子。清单15.3 的代码展现了一个简单的滚动3D图层矩阵。这看上去很酷,尤为是图层在移动的时候(见图15.1),可是绘制他们并非很麻烦,由于这些图层就是一些简单的矩形色块。
清单15.3 绘制3D图层矩阵
#import "ViewController.h"#import <QuartzCore/QuartzCore.h>#define WIDTH 10#define HEIGHT 10#define DEPTH 10#define SIZE 100#define SPACING 150#define CAMERA_DISTANCE 500@interface ViewController ()  @property (nonatomic, strong) IBOutlet UIScrollView *scrollView;@end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; //set content size self.scrollView.contentSize = CGSizeMake((WIDTH - 1)*SPACING, (HEIGHT - 1)*SPACING); //set up perspective transform CATransform3D transform = CATransform3DIdentity; transform.m34 = -1.0 / CAMERA_DISTANCE; self.scrollView.layer.sublayerTransform = transform; //create layers for (int z = DEPTH - 1; z >= 0; z--) { for (int y = 0; y < HEIGHT; y++) { for (int x = 0; x < WIDTH; x++) { //create layer CALayer *layer = [CALayer layer]; layer.frame = CGRectMake(0, 0, SIZE, SIZE); layer.position = CGPointMake(x*SPACING, y*SPACING); layer.zPosition = -z*SPACING; //set background color layer.backgroundColor = [UIColor colorWithWhite:1-z*(1.0/DEPTH) alpha:1].CGColor; //attach to scroll view [self.scrollView.layer addSublayer:layer]; } } }  //log NSLog(@"displayed: %i", DEPTH*HEIGHT*WIDTH); }@end
图15.1 滚动的3D图层矩阵
WIDTH
,HEIGHT
和DEPTH
常量控制着图层的生成。在这个状况下,咱们获得的是10*10*10个图层,总量为1000个,不过一次性显示在屏幕上的大约就几百个。
若是把WIDTH
和HEIGHT
常量增长到100,咱们的程序就会慢得像龟爬了。这样咱们有了100000个图层,性能降低一点儿也不奇怪。
可是显示在屏幕上的图层数量并无增长,那么根本没有额外的东西须要绘制。程序慢下来的缘由实际上是由于在管理这些图层上花掉了很多功夫。他们大部分对渲染的最终结果没有贡献,可是在丢弃这么图层以前,Core Animation要强制计算每一个图层的位置,就这样,咱们的帧率就慢了下来。
咱们的图层是被安排在一个均匀的栅格中,咱们能够计算出哪些图层会被最终显示在屏幕上,根本不须要对每一个图层的位置进行计算。这个计算并不简单,由于咱们还要考虑到透视的问题。若是咱们直接这样作了,Core Animation就不用费神了。
既然这样,让咱们来重构咱们的代码吧。改造后,随着视图的滚动动态地实例化图层而不是事先都分配好。这样,在创造他们以前,咱们就能够计算出是否须要他。接着,咱们增长一些代码去计算可视区域这样就能够排除区域以外的图层了。清单15.4是改造后的结果。
清单15.4 排除可视区域以外的图层
#import "ViewController.h"#import <QuartzCore/QuartzCore.h>#define WIDTH 100#define HEIGHT 100#define DEPTH 10#define SIZE 100#define SPACING 150#define CAMERA_DISTANCE 500#define PERSPECTIVE(z) (float)CAMERA_DISTANCE/(z + CAMERA_DISTANCE)@interface ViewController () <UIScrollViewDelegate>@property (nonatomic, weak) IBOutlet UIScrollView *scrollView;@end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; //set content size self.scrollView.contentSize = CGSizeMake((WIDTH - 1)*SPACING, (HEIGHT - 1)*SPACING); //set up perspective transform CATransform3D transform = CATransform3DIdentity; transform.m34 = -1.0 / CAMERA_DISTANCE; self.scrollView.layer.sublayerTransform = transform; } - (void)viewDidLayoutSubviews { [self updateLayers]; }- (void)scrollViewDidScroll:(UIScrollView *)scrollView { [self updateLayers]; }- (void)updateLayers { //calculate clipping bounds CGRect bounds = self.scrollView.bounds; bounds.origin = self.scrollView.contentOffset; bounds = CGRectInset(bounds, -SIZE/2, -SIZE/2); //create layers NSMutableArray *visibleLayers = [NSMutableArray array]; for (int z = DEPTH - 1; z >= 0; z--) { //increase bounds size to compensate for perspective CGRect adjusted = bounds; adjusted.size.width /= PERSPECTIVE(z*SPACING); adjusted.size.height /= PERSPECTIVE(z*SPACING); adjusted.origin.x -= (adjusted.size.width - bounds.size.width) / 2; adjusted.origin.y -= (adjusted.size.height - bounds.size.height) / 2; for (int y = 0; y < HEIGHT; y++) { //check if vertically outside visible rect if (y*SPACING < adjusted.origin.y || y*SPACING >= adjusted.origin.y + adjusted.size.height) { continue; } for (int x = 0; x < WIDTH; x++) { //check if horizontally outside visible rect if (x*SPACING < adjusted.origin.x ||x*SPACING >= adjusted.origin.x + adjusted.size.width) { continue; }  //create layer CALayer *layer = [CALayer layer]; layer.frame = CGRectMake(0, 0, SIZE, SIZE); layer.position = CGPointMake(x*SPACING, y*SPACING); layer.zPosition = -z*SPACING; //set background color layer.backgroundColor = [UIColor colorWithWhite:1-z*(1.0/DEPTH) alpha:1].CGColor; //attach to scroll view [visibleLayers addObject:layer]; } } } //update layers self.scrollView.layer.sublayers = visibleLayers; //log NSLog(@"displayed: %i/%i", [visibleLayers count], DEPTH*HEIGHT*WIDTH); }@end
这个计算机制并不具备普适性,可是原则上是同样。(当你用一个UITableView
或者UICollectionView
时,系统作了相似的事情)。这样作的结果?咱们的程序能够处理成百上千个『虚拟』图层并且彻底没有性能问题!由于它不须要一次性实例化几百个图层。
处理巨大数量的类似视图或图层时还有一个技巧就是回收他们。对象回收在iOS颇为常见;UITableView
和UICollectionView
都有用到,MKMapView
中的动画pin码也有用到,还有其余不少例子。
对象回收的基础原则就是你须要建立一个类似对象池。当一个对象的指定实例(本例子中指的是图层)结束了使命,你把它添加到对象池中。每次当你须要一个实例时,你就从池中取出一个。当且仅当池中为空时再建立一个新的。
这样作的好处在于避免了不断建立和释放对象(至关消耗资源,由于涉及到内存的分配和销毁)并且也没必要给类似实例重复赋值。
好了,让咱们再次更新代码吧(见清单15.5)
清单15.5 经过回收减小没必要要的分配
@interface ViewController () <UIScrollViewDelegate>@property (nonatomic, weak) IBOutlet UIScrollView *scrollView; @property (nonatomic, strong) NSMutableSet *recyclePool;@end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; //create recycle pool self.recyclePool = [NSMutableSet set]; //set content size self.scrollView.contentSize = CGSizeMake((WIDTH - 1)*SPACING, (HEIGHT - 1)*SPACING); //set up perspective transform CATransform3D transform = CATransform3DIdentity; transform.m34 = -1.0 / CAMERA_DISTANCE; self.scrollView.layer.sublayerTransform = transform; }- (void)viewDidLayoutSubviews { [self updateLayers]; }- (void)scrollViewDidScroll:(UIScrollView *)scrollView { [self updateLayers]; }- (void)updateLayers {  //calculate clipping bounds CGRect bounds = self.scrollView.bounds; bounds.origin = self.scrollView.contentOffset; bounds = CGRectInset(bounds, -SIZE/2, -SIZE/2); //add existing layers to pool [self.recyclePool addObjectsFromArray:self.scrollView.layer.sublayers]; //disable animation [CATransaction begin]; [CATransaction setDisableActions:YES]; //create layers NSInteger recycled = 0; NSMutableArray *visibleLayers = [NSMutableArray array]; for (int z = DEPTH - 1; z >= 0; z--) { //increase bounds size to compensate for perspective CGRect adjusted = bounds; adjusted.size.width /= PERSPECTIVE(z*SPACING); adjusted.size.height /= PERSPECTIVE(z*SPACING); adjusted.origin.x -= (adjusted.size.width - bounds.size.width) / 2; adjusted.origin.y -= (adjusted.size.height - bounds.size.height) / 2; for (int y = 0; y < HEIGHT; y++) { //check if vertically outside visible rect if (y*SPACING < adjusted.origin.y || y*SPACING >= adjusted.origin.y + adjusted.size.height) { continue; } for (int x = 0; x < WIDTH; x++) { //check if horizontally outside visible rect if (x*SPACING < adjusted.origin.x || x*SPACING >= adjusted.origin.x + adjusted.size.width) { continue; } //recycle layer if available CALayer *layer = [self.recyclePool anyObject]; if (layer) {  recycled ++; [self.recyclePool removeObject:layer]; } else { layer = [CALayer layer]; layer.frame = CGRectMake(0, 0, SIZE, SIZE); } //set position layer.position = CGPointMake(x*SPACING, y*SPACING); layer.zPosition = -z*SPACING; //set background color layer.backgroundColor = [UIColor colorWithWhite:1-z*(1.0/DEPTH) alpha:1].CGColor; //attach to scroll view [visibleLayers addObject:layer]; } } } [CATransaction commit]; //update layers self.scrollView.layer.sublayers = visibleLayers; //log NSLog(@"displayed: %i/%i recycled: %i", [visibleLayers count], DEPTH*HEIGHT*WIDTH, recycled); }@end
本例中,咱们只有图层对象这一种类型,可是UIKit有时候用一个标识符字符串来区分存储在不一样对象池中的不一样的可回收对象类型。
你可能注意到当设置图层属性时咱们用了一个CATransaction
来抑制动画效果。在以前并不须要这样作,由于在显示以前咱们给全部图层设置一次属性。可是既然图层正在被回收,禁止隐式动画就有必要了,否则当属性值改变时,图层的隐式动画就会被触发。
当排除掉对屏幕显示没有任何贡献的图层或者视图以后,长远看来,你可能仍然须要减小图层的数量。例如,若是你正在使用多个UILabel
或者UIImageView
实例去显示固定内容,你能够把他们所有替换成一个单独的视图,而后用-drawRect:
方法绘制出那些复杂的视图层级。
这个提议看上去并不合理由于你们都知道软件绘制行为要比GPU合成要慢并且还须要更多的内存空间,可是在由于图层数量而使得性能受限的状况下,软件绘制极可能提升性能呢,由于它避免了图层分配和操做问题。
你能够本身实验一下这个状况,它包含了性能和栅格化的权衡,可是意味着你能够从图层树上去掉子图层(用shouldRasterize
,与彻底遮挡图层相反)。
用Core Graphics去绘制一个静态布局有时候会比用层级的UIView
实例来得快,可是使用UIView
实例要简单得多并且比用手写代码写出相同效果要可靠得多,更边说Interface Builder来得直接明了。为了性能而舍弃这些便利实在是不该该。
幸亏,你没必要这样,若是大量的视图或者图层真的关联到了屏幕上将会是一个大问题。没有与图层树相关联的图层不会被送到渲染引擎,也没有性能问题(在他们被建立和配置以后)。
使用CALayer
的-renderInContext:
方法,你能够将图层及其子图层快照进一个Core Graphics上下文而后获得一个图片,它能够直接显示在UIImageView
中,或者做为另外一个图层的contents
。不一样于shouldRasterize
—— 要求图层与图层树相关联 —— ,这个方法没有持续的性能消耗。
当图层内容改变时,刷新这张图片的机会取决于你(不一样于shouldRasterize
,它自动地处理缓存和缓存验证),可是一旦图片被生成,相比于让Core Animation处理一个复杂的图层树,你节省了至关客观的性能。
本章学习了使用Core Animation图层可能遇到的性能瓶颈,并讨论了如何避免或减少压力。你学习了如何管理包含上千虚拟图层的场景(事实上只建立了几百个)。同时也学习了一些有用的技巧,选择性地选取光栅化或者绘制图层内容在合适的时候从新分配给CPU和GPU。这些就是咱们要讲的关于Core Animation的所有了(至少能够等到苹果发明什么新的玩意儿)。