图14.2 时间分析工具展现了CPU瓶颈git
这里提高性能惟一的方式就是在另外一个线程中加载图片。这并不可以下降实际的加载时间(可能状况会更糟,由于系统可能要消耗CPU时间来处理加载的图片数据),可是主线程可以有时间作一些别的事情,好比响应用户输入,以及滑动动画。github
为了在后台线程加载图片,咱们可使用GCD或者NSOperationQueue
建立自定义线程,或者使用CATiledLayer
。为了从远程网络加载图片,咱们可使用异步的NSURLConnection
,可是对本地存储的图片,并不十分有效。算法
NSOperationQueue
GCD(Grand Central Dispatch)和 NSOperationQueue 很相似,都给咱们提供了队列闭包块来在线程中按必定顺序来执行。 NSOperationQueue 有一个Objecive-C接口(而不是使用GCD的全局C函数),一样在操做优先级和依赖关系上提供了很好的粒度控制,可是须要更多地设置代码。缓存
清单14.2显示了在低优先级的后台队列而不是主线程使用GCD加载图片的 -collectionView:cellForItemAtIndexPath: 方法,而后当须要加载图片到视图的时候切换到主线程,由于在后台线程访问视图会有安全隐患。安全
因为视图在UICollectionView
会被循环利用,咱们加载图片的时候不能肯定是否被不一样的索引从新复用。为了不图片加载到错误的视图中,咱们在加载前把单元格打上索引的标签,而后在设置图片的时候检测标签是否发生了改变。网络
清单14.2 使用GCD加载传送图片闭包
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { //dequeue cell UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath]; //add image view const NSInteger imageTag = 99; UIImageView *imageView = (UIImageView *)[cell viewWithTag:imageTag]; if (!imageView) { imageView = [[UIImageView alloc] initWithFrame: cell.contentView.bounds]; imageView.tag = imageTag; [cell.contentView addSubview:imageView]; } //tag cell with index and clear current image cell.tag = indexPath.row; imageView.image = nil; //switch to background thread 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; } }); }); return cell; }
当运行更新后的版本,性能比以前不用线程的版本好多了,但仍然并不完美(图14.3)。框架
咱们能够看到 +imageWithContentsOfFile: 方法并不在CPU时间轨迹的最顶部,因此咱们的确修复了延迟加载的问题。问题在于咱们假设传送器的性能瓶颈在于图片文件的加载,但实际上并非这样。加载图片数据到内存中只是问题的第一部分。异步
图14.3 使用后台线程加载图片来提高性能async
一旦图片文件被加载就必需要进行解码,解码过程是一个至关复杂的任务,须要消耗很是长的时间。解码后的图片将一样使用至关大的内存。
用于加载的CPU时间相对于解码来讲根据图片格式而不一样。对于PNG图片来讲,加载会比JPEG更长,由于文件可能更大,可是解码会相对较快,并且Xcode会把PNG图片进行解码优化以后引入工程。JPEG图片更小,加载更快,可是解压的步骤要消耗更长的时间,由于JPEG解压算法比基于zip的PNG算法更加复杂。
当加载图片的时候,iOS一般会延迟解压图片的时间,直到加载到内存以后。这就会在准备绘制图片的时候影响性能,由于须要在绘制以前进行解压(一般是消耗时间的问题所在)。
最简单的方法就是使用UIImage
的+imageNamed:
方法避免延时加载。不像+imageWithContentsOfFile:
(和其余别的UIImage
加载方法),这个方法会在加载图片以后马上进行解压(就和本章以前咱们谈到的好处同样)。问题在于+imageNamed:
只对从应用资源束中的图片有效,因此对用户生成的图片内容或者是下载的图片就无法使用了。
另外一种马上加载图片的方法就是把它设置成图层内容,或者是UIImageView
的image
属性。不幸的是,这又须要在主线程执行,因此不会对性能有所提高。
第三种方式就是绕过UIKit
,像下面这样使用ImageIO框架:
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);
这样就可使用kCGImageSourceShouldCache
来建立图片,强制图片马上解压,而后在图片的生命周期保留解压后的版本。
最后一种方式就是使用UIKit加载图片,可是马上会知道CGContext
中去。图片必需要在绘制以前解压,因此就强制了解压的及时性。这样的好处在于绘制图片能够再后台线程(例如加载自己)执行,而不会阻塞UI。
有两种方式能够为强制解压提早渲染图片:
将图片的一个像素绘制成一个像素大小的CGContext
。这样仍然会解压整张图片,可是绘制自己并无消耗任什么时候间。这样的好处在于加载的图片并不会在特定的设备上为绘制作优化,因此能够在任什么时候间点绘制出来。一样iOS也就能够丢弃解压后的图片来节省内存了。
将整张图片绘制到CGContext
中,丢弃原始的图片,而且用一个从上下文内容中新的图片来代替。这样比绘制单一像素那样须要更加复杂的计算,可是所以产生的图片将会为绘制作优化,并且因为原始压缩图片被抛弃了,iOS就不可以随时丢弃任何解压后的图片来节省内存了。
须要注意的是苹果特别推荐了不要使用这些诡计来绕过标准图片解压逻辑(因此也是他们选择用默认处理方式的缘由),可是若是你使用不少大图来构建应用,那若是想提高性能,就只能和系统博弈了。
若是不使用+imageNamed:
,那么把整张图片绘制到CGContext
多是最佳的方式了。尽管你可能认为多余的绘制相较别的解压技术而言性能不是很高,可是新建立的图片(在特定的设备上作过优化)可能比原始图片绘制的更快。
一样,若是想显示图片到比原始尺寸小的容器中,那么一次性在后台线程从新绘制到正确的尺寸会比每次显示的时候都作缩放会更有效(尽管在这个例子中咱们加载的图片呈现正确的尺寸,因此不须要多余的优化)。
若是修改了-collectionView:cellForItemAtIndexPath:
方法来重绘图片(清单14.3),你会发现滑动更加平滑。
清单14.3 强制图片解压显示
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { //dequeue cell UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath]; ... //switch to background thread 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; } }); }); return cell; }
CATiledLayer
如第6章“专用图层”中的例子所示,CATiledLayer
能够用来异步加载和显示大型图片,而不阻塞用户输入。可是咱们一样可使用CATiledLayer
在UICollectionView
中为每一个表格建立分离的CATiledLayer
实例加载传动器图片,每一个表格仅使用一个图层。
这样使用CATiledLayer
有几个潜在的弊端:
CATiledLayer
的队列和缓存算法没有暴露出来,因此咱们只能祈祷它能匹配咱们的需求
CATiledLayer
须要咱们每次重绘图片到CGContext
中,即便它已经解压缩,并且和咱们单元格尺寸同样(所以能够直接用做图层内容,而不须要重绘)。
咱们来看看这些弊端有没有形成不一样:清单14.4显示了使用CATiledLayer
对图片传送器的从新实现。
清单14.4 使用CATiledLayer
的图片传送器
#import "ViewController.h"#import <QuartzCore/QuartzCore.h>@interface ViewController() <UICollectionViewDataSource>@property (nonatomic, copy) NSArray *imagePaths; @property (nonatomic, weak) IBOutlet UICollectionView *collectionView;@end@implementation ViewController- (void)viewDidLoad { //set up data self.imagePaths = [[NSBundle mainBundle] pathsForResourcesOfType:@"jpg" inDirectory:@"Vacation Photos"]; [self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"Cell"]; }- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return [self.imagePaths count]; }- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { //dequeue cell UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath]; //add the tiled layer CATiledLayer *tileLayer = [cell.contentView.layer.sublayers lastObject]; if (!tileLayer) { tileLayer = [CATiledLayer layer]; tileLayer.frame = cell.bounds; tileLayer.contentsScale = [UIScreen mainScreen].scale; tileLayer.tileSize = CGSizeMake(cell.bounds.size.width * [UIScreen mainScreen].scale, cell.bounds.size.height * [UIScreen mainScreen].scale); tileLayer.delegate = self; [tileLayer setValue:@(indexPath.row) forKey:@"index"]; [cell.contentView.layer addSublayer:tileLayer]; } //tag the layer with the correct index and reload tileLayer.contents = nil; [tileLayer setValue:@(indexPath.row) forKey:@"index"]; [tileLayer setNeedsDisplay]; return cell; }- (void)drawLayer:(CATiledLayer *)layer inContext:(CGContextRef)ctx { //get image index NSInteger index = [[layer valueForKey:@"index"] integerValue]; //load tile image NSString *imagePath = self.imagePaths[index]; UIImage *tileImage = [UIImage imageWithContentsOfFile:imagePath]; //calculate image rect CGFloat aspectRatio = tileImage.size.height / tileImage.size.width; CGRect imageRect = CGRectZero; imageRect.size.width = layer.bounds.size.width; imageRect.size.height = layer.bounds.size.height * aspectRatio; imageRect.origin.y = (layer.bounds.size.height - imageRect.size.height)/2; //draw tile UIGraphicsPushContext(ctx); [tileImage drawInRect:imageRect]; UIGraphicsPopContext(); }@end
须要解释几点:
CATiledLayer
的tileSize
属性单位是像素,而不是点,因此为了保证瓦片和表格尺寸一致,须要乘以屏幕比例因子。
在-drawLayer:inContext:
方法中,咱们须要知道图层属于哪个indexPath
以加载正确的图片。这里咱们利用了CALayer
的KVC来存储和检索任意的值,将图层和索引打标签。