cornerRadius
和maskToBounds
独立做用的时候都不会有太大的性能问题,可是当他俩结合在一块儿,就触发了屏幕外渲染。有时候你想显示圆角并沿着图层裁切子图层的时候,你可能会发现你并不须要沿着圆角裁切,这个状况下用CAShapeLayer
就能够避免这个问题了。git
你想要的只是圆角且沿着矩形边界裁切,同时还不但愿引发性能问题。其实你能够用现成的UIBezierPath
的构造器 +bezierPathWithRoundedRect:cornerRadius: (见清单15.1).这样作并不会比直接用cornerRadius
更快,可是它避免了性能问题。github
清单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网格渲染而成),然而,许多都须要渲染成一个顺滑的曲线。在实际应用上,两者并无太大的区别。ide
清单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会放弃绘制那些彻底被其余图层遮挡的像素,可是要计算出一个图层是否被遮挡也是至关复杂而且会消耗处理器资源。一样,合并不一样图层的透明重叠像素(即混合)消耗的资源也是至关客观的。因此为了加速处理进程,不到必须时刻不要使用透明图层。任何状况下,你应该这样作:ui
给视图的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
图层性能
要更快性能,也要作对正确的事情。 ——Stephen R. Covey
本例中,咱们只有图层对象这一种类型,可是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的所有了(至少能够等到苹果发明什么新的玩意儿)。