iOS Core Animation Advanced Techniques-图层性能
上十四章节:
这篇随笔主要介绍有关图层性能。
隐式绘制:
- 寄宿图:
- 可以通过Core Graphics直接绘制
- 也可以直接载入一个图片文件并赋值给contents属性
- 或事先绘制一个屏幕之外的CGContext上下文。
- 可以通过以下三种方式创建隐式的:
- 1,使用特性的图层属性。
- 2,特定的视图。
- 3,特定的图层子类。
文本:
- CATextLayer和UILabel都是直接将文本绘制在图层的寄宿图中。
- 事实上这两种方式用了完全不同的渲染方式:
- iOS 6及之前,UILabel用WebKit的HTML渲染引擎来绘制文本
- CATextLayer用的是Core Text(渲染更迅速,需要绘制大量文本的情形下都优先使用它)
- 尽可能地避免改变那些包含文本的视图的frame,因为这样做的话文本就需要重绘。例如,如果你想在图层的角落里显示一段静态的文本,
- 但是这个图层经常改动,你就应该把文本放在一个子图层中。
光栅栏:
- 『视觉效果』一章提到了CALayer的shouldRasterize属性,它可以解决重叠透明图层的混合失灵问题。
- 『速度的曲调』中,它也是作为绘制复杂图层树结构的优化方法。
- 启用shouldRasterize属性会将图层绘制到一个屏幕之外的图像。
- 然后这个图像将会被缓存起来并绘制到实际图层的contents和子图层。
- 如果有很多的子图层或者有复杂的效果应用,这样做就会比重绘所有事务的所有帧划得来得多。
- 缺点:
- 光栅化原始图像需要时间,而且还会消耗额外的内存。
- 不应作用在内容不断变动的图层上,否则它缓存方面的好处就会消失,而且会让性能变的更糟。
- 可以通过
- Instrument查看一下Color Hits Green和Misses Red项目,是否已光栅化图像被频繁地刷新
- 这样就说明图层并不是光栅化的好选择,或则你无意间触发了不必要的改变导致了重绘行为
离屏渲染:
- 当图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制时,屏幕外渲染就被唤起了。
- 意味着图层必须在被显示之前在一个屏幕外上下文中被渲染
- 图层的以下属性将会触发屏幕外绘制:
- 1.圆角(当和maskToBounds一起使用时)
- 2.图层蒙板
- 3.阴影
- 与光栏栅区别:
- 没有像光栅化图层那么消耗大
- 子图层并没有被影响到
- 结果也没有被缓存,所以不会有长期的内存占用。
- 如果太多图层在屏幕外渲染依然会影响到性能。
- 对于那些需要动画而且要在屏幕外渲染的图层来说
- 可以用CAShapeLayer,contentsCenter或者shadowPath来获得同样的表现而且较少地影响到性能。
- CAShapeLayer:
- cornerRadius和maskToBounds独立作用的时候都不会有太大的性能问题,但是当他俩结合在一起,就触发了屏幕外渲染。
- 有时候想显示圆角并沿着图层裁切子图层的时候,你可能会发现你并不需要沿着圆角裁切,这个情况下用CAShapeLayer就可以避免这个问题了。
- 也可以用现成的UIBezierPath的构造器+bezierPathWithRoundedRect:cornerRadius:
- 示范例子:
- //用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
可伸缩图片:
- 另一个创建圆角矩形的方法就是用一个圆形内容图片并结合第二章『寄宿图』提到的contensCenter属性去创建一个可伸缩图片,.理论上来说,这个应该比用CAShapeLayer要快,在实际应用上,二者并没有太大的区别。
- 示范例子:
- // 用可伸缩图片绘制圆角矩形
- @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
- /*
- 使用可伸缩图片的优势在于它可以绘制成任意边框效果而不需要额外的性能消耗。
- 可伸缩图片甚至还可以显示出矩形阴影的效果。如下
- */
shadowPaht:
- 如第二章所提及的。如果图层是一个简单几何图形如矩形或者圆角矩形(假设不包含任何透明部分或者子图层),
- 创建出一个对应形状的阴影路径就比较容易,
- 而且Core Animation绘制这个阴影也相当简单,避免了屏幕外的图层部分的预排版需求。
- 这对性能来说很有帮助。
- 如果你的图层是一个更复杂的图形,生成正确的阴影路径可能就比较难了,这样子的话你可以考虑用绘图软件预先生成一个阴影背景图。
混合和过度绘制:
- 混合:
- 合并不同图层的透明重叠像素
- 混合消耗资源也是相当大的。
- 为了加速处理进程,不到必须时刻不要使用透明图层。任何情况下,你应该这样做:
- 1.给视图的backgroundColor属性设置一个固定的,不透明的颜色
- 2.设置opaque属性为YES
- 如果用到了图像,尽量避免透明除非非常必要。
- 如果图像要显示在一个固定的背景颜色或是固定的背景图之前,你没必要相对前景移动,你只需要预填充背景图片就可以避免运行时混色了。
- 如果是文本的话,一个白色背景的UILabel(或者其他颜色)会比透明背景要更高效。
- 明智地使用shouldRasterize属性,可以将一个固定的图层体系折叠成单张图片,这样就不需要每一帧重新合成了,也就不会有因为子图层之间的混合和过度绘制的性能问题了。
- 减少图层数量:
- 一个图层的大致资源开销:
- 初始化图层
- 处理图层
- 打包通过IPC发给渲染引擎
- 转化成OpenGL几何图形
裁切:
- 对图层做任何优化之前,需要确定创建的图层都是可见的,避免创建和配置不必要图层的额外工作。
- 图层在以下集中情况下会不可见:
- 1.图层在屏幕边界之外,或是在父图层边界之外。
- 2.完全在一个不透明图层之后。
- 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
- //例子讲解:
- WIDTH,HEIGHT和DEPTH常量控制着图层的生成,这里,我们得到的是10*10*10个图层,总量为1000个,不过一次性显示在屏幕上的大约就几百个。
- 如果把WIDTH和HEIGHT常量增加到100,我们的程序就会慢得像龟爬了。
- 但是显示在屏幕上的图层数量并没有增加,那么根本没有额外的东西需要绘制。
- 即使屏幕外的一些图层没有被显示,但是Core Animation要强制计算每个图层的位置,就这样,我们的帧率就慢了下来。
- 优化方案:
- 图层是被安排在一个均匀的栅格中,我们可以计算出哪些图层会被最终显示在屏幕上,根本不需要对每个图层的位置进行计算。
- 优化例子:
- //随着视图的滚动动态地实例化图层而不是事先都分配好
- #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
对象回收:
- 处理巨大数量的相似视图或图层时还有一个技巧就是回收他们。
- 对象回收的基础原则就是你需要创建一个相似对象池。
- 当一个对象的指定实例(本例子中指的是图层)结束了使命,你把它添加到对象池中。
- 每次当你需要一个实例时,你就从池中取出一个。
- 当且仅当池中为空时再创建一个新的。
- 好处:
- 避免了不断创建和释放对象(相当消耗资源,因为涉及到内存的分配和销毁)而且也不必给相似实例重复赋值。
- 示范例子://使用上面例子通过回收减少不必要的分配
- @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
Core Graphics绘制:
- 长远看来,需要减少图层的数量。
- 例如:
- 如果正在使用多个UILabel或者UIImageView实例去显示固定内容,
- 可以把他们全部替换成一个单独的视图,
- 然后用-drawRect:方法绘制出那些复杂的视图层级。
- -renderInContext: 方法:
- 可以将图层及其子图层快照进一个Core Graphics上下文然后得到一个图片,它可以直接显示在UIImageView中,或者作为另一个图层的contents。
- 一旦图片被生成,相比于让Core Animation处理一个复杂的图层树,节省了相当客观的性能。