圖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來存儲和檢索任意的值,將圖層和索引打標籤。