iOS-Core-Animation-Advanced-Techniques(七-4)

文件格式

图片加载性能取决于加载大图的时间和解压小图时间的权衡。很多苹果的文档都说PNG是iOS所有图片加载的最好格式。但这是极度误导的过时信息了。

PNG图片使用的无损压缩算法可以比使用JPEG的图片做到更快地解压,但是由于闪存访问的原因,这些加载的时间并没有什么区别。

清单14.6展示了标准的应用程序加载不同尺寸图片所需要时间的一些代码。为了保证实验的准确性,我们会测量每张图片的加载和绘制时间来确保考虑到解压性能的因素。另外每隔一秒重复加载和绘制图片,这样就可以取到平均时间,使得结果更加准确。

清单14.6

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#import "ViewController.h"
static NSString *const ImageFolder = @ "Coast Photos" ;
@interface ViewController () @property (nonatomic, copy) NSArray *items;
@property (nonatomic, weak) IBOutlet UITableView *tableView;
@end
@implementation ViewController
- (void)viewDidLoad
{
     [ super  viewDidLoad];
     //set up image names
     self.items = @[@ "2048x1536" , @ "1024x768" , @ "512x384" , @ "256x192" , @ "128x96" , @ "64x48" , @ "32x24" ];
}
- (CFTimeInterval)loadImageForOneSec:(NSString *)path
{
     //create drawing context to use for decompression
     UIGraphicsBeginImageContext(CGSizeMake(1, 1));
     //start timing
     NSInteger imagesLoaded = 0;
     CFTimeInterval endTime = 0;
     CFTimeInterval startTime = CFAbsoluteTimeGetCurrent();
     while  (endTime - startTime < 1) {
         //load image
         UIImage *image = [UIImage imageWithContentsOfFile:path];
         //decompress image by drawing it
         [image drawAtPoint:CGPointZero];
         //update totals
         imagesLoaded ++;
         endTime = CFAbsoluteTimeGetCurrent();
     }
     //close context
     UIGraphicsEndImageContext();
     //calculate time per image
     return  (endTime - startTime) / imagesLoaded;
}
- (void)loadImageAtIndex:(NSUInteger)index
{
     //load on background thread so as not to
     //prevent the UI from updating between runs dispatch_async(
     dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
         //setup
         NSString *fileName = self.items[index];
         NSString *pngPath = [[NSBundle mainBundle] pathForResource:filename
                                                             ofType:@ "png"
                                                        inDirectory:ImageFolder];
         NSString *jpgPath = [[NSBundle mainBundle] pathForResource:filename
                                                             ofType:@ "jpg"
                                                        inDirectory:ImageFolder];
         //load
         NSInteger pngTime = [self loadImageForOneSec:pngPath] * 1000;
         NSInteger jpgTime = [self loadImageForOneSec:jpgPath] * 1000;
         //updated UI on main thread
         dispatch_async(dispatch_get_main_queue(), ^{
             //find table cell and update
             NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
             UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
             cell.detailTextLabel.text = [NSString stringWithFormat:@ "PNG: ims JPG: ims" , pngTime, jpgTime];
         });
     });
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
     return  [self.items count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
     //dequeue cell
     UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@ "Cell" ];
     if  (!cell) {
         cell = [[UITableViewCell alloc] initWithStyle: UITableViewCellStyleValue1 reuseIdentifier:@ "Cell" ];
     }
     //set up cell
     NSString *imageName = self.items[indexPath.row];
     cell.textLabel.text = imageName;
     cell.detailTextLabel.text = @ "Loading..." ;
     //load image
     [self loadImageAtIndex:indexPath.row];
     return  cell;
}
@end

PNG和JPEG压缩算法作用于两种不同的图片类型:JPEG对于噪点大的图片效果很好;但是PNG更适合于扁平颜色,锋利的线条或者一些渐变色的图片。为了让测评的基准更加公平,我们用一些不同的图片来做实验:一张照片和一张彩虹色的渐变。JPEG版本的图片都用默认的Photoshop60%“高质量”设置编码。结果见图片14.5。

14.5.jpg

图14.5 不同类型图片的相对加载性能

如结果所示,相对于不友好的PNG图片,相同像素的JPEG图片总是比PNG加载更快,除非一些非常小的图片、但对于友好的PNG图片,一些中大尺寸的图效果还是很好的。

所以对于之前的图片传送器程序来说,JPEG会是个不错的选择。如果用JPEG的话,一些多线程和缓存策略都没必要了。

但JPEG图片并不是所有情况都适用。如果图片需要一些透明效果,或者压缩之后细节损耗很多,那就该考虑用别的格式了。苹果在iOS系统中对PNG和JPEG都做了一些优化,所以普通情况下都应该用这种格式。也就是说在一些特殊的情况下才应该使用别的格式。

混合图片

对于包含透明的图片来说,最好是使用压缩透明通道的PNG图片和压缩RGB部分的JPEG图片混合起来加载。这就对任何格式都适用了,而且无论从质量还是文件尺寸还是加载性能来说都和PNG和JPEG的图片相近。相关分别加载颜色和遮罩图片并在运行时合成的代码见14.7。

清单14.7 从PNG遮罩和JPEG创建的混合图片

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
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIImageView *imageView;
@end
@implementation ViewController
- (void)viewDidLoad
{
     [ super  viewDidLoad];
     //load color image
     UIImage *image = [UIImage imageNamed:@ "Snowman.jpg" ];
     //load mask image
     UIImage *mask = [UIImage imageNamed:@ "SnowmanMask.png" ];
     //convert mask to correct format
     CGColorSpaceRef graySpace = CGColorSpaceCreateDeviceGray();
     CGImageRef maskRef = CGImageCreateCopyWithColorSpace(mask.CGImage, graySpace);
     CGColorSpaceRelease(graySpace);
     //combine images
     CGImageRef resultRef = CGImageCreateWithMask(image.CGImage, maskRef);
     UIImage *result = [UIImage imageWithCGImage:resultRef];
     CGImageRelease(resultRef);
     CGImageRelease(maskRef);
     //display result
     self.imageView.image = result;
}
@end

对每张图片都使用两个独立的文件确实有些累赘。JPNG的库(https://github.com/nicklockwood/JPNG)对这个技术提供了一个开源的可以复用的实现,并且添加了直接使用+imageNamed:和+imageWithContentsOfFile:方法的支持。

JPEG 2000

除了JPEG和PNG之外iOS还支持别的一些格式,例如TIFF和GIF,但是由于他们质量压缩得更厉害,性能比JPEG和PNG糟糕的多,所以大多数情况并不用考虑。

但是iOS之后,苹果低调添加了对JPEG 2000图片格式的支持,所以大多数人并不知道。它甚至并不被Xcode很好的支持 - JPEG 2000图片都没在Interface Builder中显示。

但是JPEG 2000图片在(设备和模拟器)运行时会有效,而且比JPEG质量更好,同样也对透明通道有很好的支持。但是JPEG 2000图片在加载和显示图片方面明显要比PNG和JPEG慢得多,所以对图片大小比运行效率更敏感的时候,使用它是一个不错的选择。

但仍然要对JPEG 2000保持关注,因为在后续iOS版本说不定就对它的性能做提升,但是在现阶段,混合图片对更小尺寸和质量的文件性能会更好。

PVRTC

当前市场的每个iOS设备都使用了Imagination Technologies PowerVR图像芯片作为GPU。PowerVR芯片支持一种叫做PVRTC(PowerVR Texture Compression)的标准图片压缩。

和iOS上可用的大多数图片格式不同,PVRTC不用提前解压就可以被直接绘制到屏幕上。这意味着在加载图片之后不需要有解压操作,所以内存中的图片比其他图片格式大大减少了(这取决于压缩设置,大概只有1/60那么大)。

但是PVRTC仍然有一些弊端:

尽管加载的时候消耗了更少的RAM,PVRTC文件比JPEG要大,有时候甚至比PNG还要大(这取决于具体内容),因为压缩算法是针对于性能,而不是文件尺寸。

PVRTC必须要是二维正方形,如果源图片不满足这些要求,那必须要在转换成PVRTC的时候强制拉伸或者填充空白空间。

质量并不是很好,尤其是透明图片。通常看起来更像严重压缩的JPEG文件。

PVRTC不能用Core Graphics绘制,也不能在普通的UIImageView显示,也不能直接用作图层的内容。你必须要用作OpenGL纹理加载PVRTC图片,然后映射到一对三角板来在CAEAGLLayer或者GLKView中显示。

创建一个OpenGL纹理来绘制PVRTC图片的开销相当昂贵。除非你想把所有图片绘制到一个相同的上下文,不然这完全不能发挥PVRTC的优势。

PVRTC使用了一个不对称的压缩算法。尽管它几乎立即解压,但是压缩过程相当漫长。在一个现代快速的桌面Mac电脑上,它甚至要消耗一分钟甚至更多来生成一个PVRTC大图。因此在iOS设备上最好不要实时生成。

如果你愿意使用OpehGL,而且即使提前生成图片也能忍受得了,那么PVRTC将会提供相对于别的可用格式来说非常高效的加载性能。比如,可以在主线程1/60秒之内加载并显示一张2048×2048的PVRTC图片(这已经足够大来填充一个视网膜屏幕的iPad了),这就避免了很多使用线程或者缓存等等复杂的技术难度。

Xcode包含了一些命令行工具例如texturetool来生成PVRTC图片,但是用起来很不方便(它存在于Xcode应用程序束中),而且很受限制。一个更好的方案就是使用Imagination Technologies PVRTexTool,可以从http://www.imgtec.com/powervr/insider/sdkdownloads免费获得。

安装了PVRTexTool之后,就可以使用如下命令在终端中把一个合适大小的PNG图片转换成PVRTC文件:

/Applications/Imagination/PowerVR/GraphicsSDK/PVRTexTool/CL/OSX_x86/PVRTexToolCL -i {input_file_name}.png -o {output_file_name}.pvr -legacypvr -p -f PVRTC1_4 -q pvrtcbest

清单14.8的代码展示了加载和显示PVRTC图片的步骤(第6章CAEAGLLayer例子代码改动而来)。

清单14.8 加载和显示PVRTC图片

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#import "ViewController.h" 
#import  
#import @interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *glView;
@property (nonatomic, strong) EAGLContext *glContext;
@property (nonatomic, strong) CAEAGLLayer *glLayer;
@property (nonatomic, assign) GLuint framebuffer;
@property (nonatomic, assign) GLuint colorRenderbuffer;
@property (nonatomic, assign) GLint framebufferWidth;
@property (nonatomic, assign) GLint framebufferHeight;
@property (nonatomic, strong) GLKBaseEffect *effect;
@property (nonatomic, strong) GLKTextureInfo *textureInfo;
@end
@implementation ViewController
- (void)setUpBuffers
{
     //set up frame buffer
     glGenFramebuffers(1, &_framebuffer);
     glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);
     //set up color render buffer
     glGenRenderbuffers(1, &_colorRenderbuffer);
     glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);
     glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorRenderbuffer);
     [self.glContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.glLayer];
     glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &_framebufferWidth);
     glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_framebufferHeight);
     //check success
     if  (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
         NSLog(@ "Failed to make complete framebuffer object: %i" , glCheckFramebufferStatus(GL_FRAMEBUFFER));
     }
}
- (void)tearDownBuffers
{
     if  (_framebuffer) {
         //delete framebuffer
         glDeleteFramebuffers(1, &_framebuffer);
         _framebuffer = 0;
     }
     if  (_colorRenderbuffer) {
         //delete color render buffer
         glDeleteRenderbuffers(1, &_colorRenderbuffer);
         _colorRenderbuffer = 0;
     }
}
- (void)drawFrame
{
     //bind framebuffer & set viewport
     glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);
     glViewport(0, 0, _framebufferWidth, _framebufferHeight);
     //bind shader program
     [self.effect prepareToDraw];
     //clear the screen
     glClear(GL_COLOR_BUFFER_BIT);
     glClearColor(0.0, 0.0, 0.0, 0.0);
     //set up vertices
     GLfloat vertices[] = {
         -1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f
     };
     //set up colors
     GLfloat texCoords[] = {
         0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f
     };
     //draw triangle
     glEnableVertexAttribArray(GLKVertexAttribPosition);
     glEnableVertexAttribArray(GLKVertexAttribTexCoord0);
     glVertexAttribPointer(GLKVertexAttribPosition, 2, GL_FLOAT, GL_FALSE, 0, vertices);
     glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, 0, texCoords);
     glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
     //present render buffer
     glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);
     [self.glContext presentRenderbuffer:GL_RENDERBUFFER];
}
- (void)viewDidLoad
{
     [ super  viewDidLoad];
     //set up context
     self.glContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
     [EAGLContext setCurrentContext:self.glContext];
     //set up layer
     self.glLayer = [CAEAGLLayer layer];
     self.glLayer.frame = self.glView.bounds;
     self.glLayer.opaque = NO;
     [self.glView.layer addSublayer:self.glLayer];
     self.glLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking: @NO, kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8};
     //load texture
     glActiveTexture(GL_TEXTURE0);
     NSString *imageFile = [[NSBundle mainBundle] pathForResource:@ "Snowman"  ofType:@ "pvr" ];
     self.textureInfo = [GLKTextureLoader textureWithContentsOfFile:imageFile options:nil error:NULL];
     //create texture
     GLKEffectPropertyTexture *texture = [[GLKEffectPropertyTexture alloc] init];
     texture.enabled = YES;
     texture.envMode = GLKTextureEnvModeDecal;
     texture.name = self.textureInfo.name;
     //set up base effect
     self.effect = [[GLKBaseEffect alloc] init];
     self.effect.texture2d0.name = texture.name;
     //set up buffers
     [self setUpBuffers];
     //draw frame
     [self drawFrame];
}
- (void)viewDidUnload
{
     [self tearDownBuffers];
     [ super  viewDidUnload];
}
- (void)dealloc
{
     [self tearDownBuffers];
     [EAGLContext setCurrentContext:nil];
}
@end

如你所见,非常不容易,如果你对在常规应用中使用PVRTC图片很感兴趣的话(例如基于OpenGL的游戏),可以参考一下GLView的库(https://github.com/nicklockwood/GLView),它提供了一个简单的GLImageView类,重新实现了UIImageView的各种功能,但同时提供了PVRTC图片,而不需要你写任何OpenGL代码。

总结

在这章中,我们研究了和图片加载解压相关的性能问题,并延展了一系列解决方案。

在第15章“图层性能”中,我们将讨论和图层渲染和组合相关的性能问题。

--------------------------------------------------------------------------------------------------------------------------------------------------------

图层性能

要更快性能,也要做对正确的事情。 ——Stephen R. Covey

在第14章『图像IO』讨论如何高效地载入和显示图像,通过视图来避免可能引起动画帧率下降的性能问题。在最后一章,我们将着重图层树本身,以发掘最好的性能。

隐式绘制

寄宿图可以通过Core Graphics直接绘制,也可以直接载入一个图片文件并赋值给contents属性,或事先绘制一个屏幕之外的CGContext上下文。在之前的两章中我们讨论了这些场景下的优化。但是除了常见的显式创建寄宿图,你也可以通过以下三种方式创建隐式的:1,使用特性的图层属性。2,特定的视图。3,特定的图层子类。

了解这个情况为什么发生何时发生是很重要的,它能够让你避免引入不必要的软件绘制行为。

文本

CATextLayer和UILabel都是直接将文本绘制在图层的寄宿图中。事实上这两种方式用了完全不同的渲染方式:在iOS 6及之前,UILabel用WebKit的HTML渲染引擎来绘制文本,而CATextLayer用的是Core Text.后者渲染更迅速,所以在所有需要绘制大量文本的情形下都优先使用它吧。但是这两种方法都用了软件的方式绘制,因此他们实际上要比硬件加速合成方式要慢。

不论如何,尽可能地避免改变那些包含文本的视图的frame,因为这样做的话文本就需要重绘。例如,如果你想在图层的角落里显示一段静态的文本,但是这个图层经常改动,你就应该把文本放在一个子图层中。

光栅化

在第四章『视觉效果』中我们提到了CALayer的shouldRasterize属性,它可以解决重叠透明图层的混合失灵问题。同样在第12章『速度的曲调』中,它也是作为绘制复杂图层树结构的优化方法。

启用shouldRasterize属性会将图层绘制到一个屏幕之外的图像。然后这个图像将会被缓存起来并绘制到实际图层的contents和子图层。如果有很多的子图层或者有复杂的效果应用,这样做就会比重绘所有事务的所有帧划得来得多。但是光栅化原始图像需要时间,而且还会消耗额外的内存。

当我们使用得当时,光栅化可以提供很大的性能优势(如你在第12章所见),但是一定要避免作用在内容不断变动的图层上,否则它缓存方面的好处就会消失,而且会让性能变的更糟。

为了检测你是否正确地使用了光栅化方式,用Instrument查看一下Color Hits Green和Misses Red项目,是否已光栅化图像被频繁地刷新(这样就说明图层并不是光栅化的好选择,或则你无意间触发了不必要的改变导致了重绘行为)。

屏幕外渲染

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

  • 圆角(当和maskToBounds一起使用时)

  • 图层蒙板

  • 阴影

屏幕外渲染和我们启用光栅化时相似,除了它并没有像光栅化图层那么消耗大,子图层并没有被影响到,而且结果也没有被缓存,所以不会有长期的内存占用。但是,如果太多图层在屏幕外渲染依然会影响到性能。

有时候我们可以把那些需要屏幕外绘制的图层开启光栅化以作为一个优化方式,前提是这些图层并不会被频繁地重绘。

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

CAShapeLayer

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

你想要的只是圆角且沿着矩形边界裁切,同时还不希望引起性能问题。其实你可以用现成的UIBezierPath的构造器+bezierPathWithRoundedRect:cornerRadius:(见清单15.1).这样做并不会比直接用cornerRadius更快,但是它避免了性能问题。

清单15.2 用CAShapeLayer画一个圆角矩形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#import "ViewController.h"
#import @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属性去创建一个可伸缩图片(见清单15.2).理论上来说,这个应该比用CAShapeLayer要快,因为一个可拉伸图片只需要18个三角形(一个图片是由一个3*3网格渲染而成),然而,许多都需要渲染成一个顺滑的曲线。在实际应用上,二者并没有太大的区别。

清单15.2 用可伸缩图片绘制圆角矩形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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

使用可伸缩图片的优势在于它可以绘制成任意边框效果而不需要额外的性能消耗。举个例子,可伸缩图片甚至还可以显示出矩形阴影的效果。

shadowPath

在第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图层矩阵

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
27
28
29
30
31
32
33
34
35
36
37
38
39
#import "ViewController.h"
#import #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.jpg

图15.1 滚动的3D图层矩阵

WIDTH,HEIGHT和DEPTH常量控制着图层的生成。在这个情况下,我们得到的是10*10*10个图层,总量为1000个,不过一次性显示在屏幕上的大约就几百个。

如果把WIDTH和HEIGHT常量增加到100,我们的程序就会慢得像龟爬了。这样我们有了100000个图层,性能下降一点儿也不奇怪。

但是显示在屏幕上的图层数量并没有增加,那么根本没有额外的东西需要绘制。程序慢下来的原因其实是因为在管理这些图层上花掉了不少功夫。他们大部分对渲染的最终结果没有贡献,但是在丢弃这么图层之前,Core Animation要强制计算每个图层的位置,就这样,我们的帧率就慢了下来。

我们的图层是被安排在一个均匀的栅格中,我们可以计算出哪些图层会被最终显示在屏幕上,根本不需要对每个图层的位置进行计算。这个计算并不简单,因为我们还要考虑到透视的问题。如果我们直接这样做了,Core Animation就不用费神了。

既然这样,让我们来重构我们的代码吧。改造后,随着视图的滚动动态地实例化图层而不是事先都分配好。这样,在创造他们之前,我们就可以计算出是否需要他。接着,我们增加一些代码去计算可视区域这样就可以排除区域之外的图层了。清单15.4是改造后的结果。

清单15.4 排除可视区域之外的图层

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#import "ViewController.h"
#import #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 () @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 通过回收减少不必要的分配

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
@interface ViewController () @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.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来抑制动画效果。在之前并不需要这样做,因为在显示之前我们给所有图层设置一次属性。但是既然图层正在被回收,禁止隐式动画就有必要了,不然当属性值改变时,图层的隐式动画就会被触发。

Core Graphics绘制

当排除掉对屏幕显示没有任何贡献的图层或者视图之后,长远看来,你可能仍然需要减少图层的数量。例如,如果你正在使用多个UILabel或者UIImageView实例去显示固定内容,你可以把他们全部替换成一个单独的视图,然后用-drawRect:方法绘制出那些复杂的视图层级。

这个提议看上去并不合理因为大家都知道软件绘制行为要比GPU合成要慢而且还需要更多的内存空间,但是在因为图层数量而使得性能受限的情况下,软件绘制很可能提高性能呢,因为它避免了图层分配和操作问题。

你可以自己实验一下这个情况,它包含了性能和栅格化的权衡,但是意味着你可以从图层树上去掉子图层(用shouldRasterize,与完全遮挡图层相反)。

-renderInContext: 方法

用Core Graphics去绘制一个静态布局有时候会比用层级的UIView实例来得快,但是使用UIView实例要简单得多而且比用手写代码写出相同效果要可靠得多,更边说Interface Builder来得直接明了。为了性能而舍弃这些便利实在是不应该。

幸好,你不必这样,如果大量的视图或者图层真的关联到了屏幕上将会是一个大问题。没有与图层树相关联的图层不会被送到渲染引擎,也没有性能问题(在他们被创建和配置之后)。

使用CALayer的-renderInContext:方法,你可以将图层及其子图层快照进一个Core Graphics上下文然后得到一个图片,它可以直接显示在UIImageView中,或者作为另一个图层的contents。不同于shouldRasterize —— 要求图层与图层树相关联 —— ,这个方法没有持续的性能消耗。

当图层内容改变时,刷新这张图片的机会取决于你(不同于shouldRasterize,它自动地处理缓存和缓存验证),但是一旦图片被生成,相比于让Core Animation处理一个复杂的图层树,你节省了相当客观的性能。

总结

本章学习了使用Core Animation图层可能遇到的性能瓶颈,并讨论了如何避免或减小压力。你学习了如何管理包含上千虚拟图层的场景(事实上只创建了几百个)。同时也学习了一些有用的技巧,选择性地选取光栅化或者绘制图层内容在合适的时候重新分配给CPU和GPU。这些就是我们要讲的关于Core Animation的全部了(至少可以等到苹果发明什么新的玩意儿)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值