潛伏期值得思考 - 凱文 帕薩特git
在第13章「高效繪圖」中,咱們研究了和Core Graphics繪圖相關的性能問題,以及如何修復。和繪圖性能相關緊密相關的是圖像性能。在這一章中,咱們將研究如何優化從閃存驅動器或者網絡中加載和顯示圖片。github
繪圖實際消耗的時間一般並非影響性能的因素。圖片消耗很大一部份內存,並且不太可能把須要顯示的圖片都保留在內存中,因此須要在應用運行的時候週期性地加載和卸載圖片。objective-c
圖片文件加載的速度被CPU和IO(輸入/輸出)同時影響。iOS設備中的閃存已經比傳統硬盤快不少了,但仍然比RAM慢將近200倍左右,這就須要很當心地管理加載,來避免延遲。算法
只要有可能,試着在程序生命週期不易察覺的時候來加載圖片,例如啓動,或者在屏幕切換的過程當中。按下按鈕和按鈕響應事件之間最大的延遲大概是200ms,這比動畫每一幀切換的16ms小得多。你能夠在程序首次啓動的時候加載圖片,可是若是20秒內沒法啓動程序的話,iOS檢測計時器就會終止你的應用(並且若是啓動大於2,3秒的話用戶就會抱怨了)。緩存
有些時候,提早加載全部的東西並不明智。好比說包含上千張圖片的圖片傳送帶:用戶但願可以可以平滑快速翻動圖片,因此就不可能提早預加載全部圖片;那樣會消耗太多的時間和內存。安全
有時候圖片也須要從遠程網絡鏈接中下載,這將會比從磁盤加載要消耗更多的時間,甚至可能因爲鏈接問題而加載失敗(在幾秒鐘嘗試以後)。你不可以在主線程中加載網絡形成等待,因此須要後臺線程。網絡
在第12章「性能調優」咱們的聯繫人列表例子中,圖片都很是小,因此能夠在主線程同步加載。可是對於大圖來講,這樣作就不太合適了,由於加載會消耗很長時間,形成滑動的不流暢。滑動動畫會在主線程的run loop中更新,因此會有更多運行在渲染服務進程中CPU相關的性能問題。多線程
清單14.1顯示了一個經過 UICollectionView 實現的基礎的圖片傳送器。圖片在主線程中 -collectionView:cellForItemAtIndexPath: 方法中同步加載(見圖14.1)。閉包
清單14.1 使用UICollectionView
實現的圖片傳送器框架
#import "ViewController.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:@"png" inDirectory:@"Vacation Photos"]; //register cell class [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 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]; } //set image NSString *imagePath = self.imagePaths[indexPath.row]; imageView.image = [UIImage imageWithContentsOfFile:imagePath]; return cell; } @end
![圖14.1 圖14.1](http://static.javashuo.com/static/loading.gif)
圖14.1 運行中的圖片傳送器
傳送器中的圖片尺寸爲800x600像素的PNG,對iPhone5來講,1/60秒要加載大概700KB左右的圖片。當傳送器滾動的時候,圖片也在實時加載,因而(預期中的)卡動就發生了。時間分析工具(圖14.2)顯示了不少時間都消耗在了 UIImage 的 +imageWithContentsOfFile: 方法中了。很明顯,圖片加載形成了瓶頸。
圖14.2 時間分析工具展現了CPU瓶頸
這裏提高性能惟一的方式就是在另外一個線程中加載圖片。這並不可以下降實際的加載時間(可能狀況會更糟,由於系統可能要消耗CPU時間來處理加載的圖片數據),可是主線程可以有時間作一些別的事情,好比響應用戶輸入,以及滑動動畫。
爲了在後臺線程加載圖片,咱們可使用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 使用後臺線程加載圖片來提高性能
一旦圖片文件被加載就必需要進行解碼,解碼過程是一個至關複雜的任務,須要消耗很是長的時間。解碼後的圖片將一樣使用至關大的內存。
用於加載的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來存儲和檢索任意的值,將圖層和索引打標籤。
結果CATiledLayer
工做的很好,性能問題解決了,並且和用GCD實現的代碼量差很少。僅有一個問題在於圖片加載到屏幕上後有一個明顯的淡入(圖14.4)。
圖14.4 加載圖片以後的淡入
咱們能夠調整CATiledLayer
的fadeDuration
屬性來調整淡入的速度,或者直接將整個漸變移除,可是這並無根本性地去除問題:在圖片加載到準備繪製的時候總會有一個延遲,這將會致使滑動時候新圖片的跳入。這並非CATiledLayer
的問題,使用GCD的版本也有這個問題。
即便使用上述咱們討論的全部加載圖片和緩存的技術,有時候仍然會發現實時加載大圖仍是有問題。就和13章中提到的那樣,iPad上一整個視網膜屏圖片分辨率達到了2048x1536,並且會消耗12MB的RAM(未壓縮)。第三代iPad的硬件並不能支持1/60秒的幀率加載,解壓和顯示這種圖片。即便用後臺線程加載來避免動畫卡頓,仍然解決不了問題。
咱們能夠在加載的同時顯示一個佔位圖片,但這並無根本解決問題,咱們能夠作到更好。
視網膜分辨率(根據蘋果市場定義)表明了人的肉眼在正常視角距離可以分辨的最小像素尺寸。可是這隻能應用於靜態像素。當觀察一個移動圖片時,你的眼睛就會對細節不敏感,因而一個低分辨率的圖片和視網膜質量的圖片沒什麼區別了。
若是須要快速加載和顯示移動大圖,簡單的辦法就是欺騙人眼,在移動傳送器的時候顯示一個小圖(或者低分辨率),而後當中止的時候再換成大圖。這意味着咱們須要對每張圖片存儲兩份不一樣分辨率的副本,可是幸運的是,因爲須要同時支持Retina和非Retina設備,原本這就是廣泛要作到的。
若是從遠程源或者用戶的相冊加載沒有可用的低分辨率版本圖片,那就能夠動態將大圖繪製到較小的CGContext
,而後存儲到某處以備複用。
爲了作到圖片交換,咱們須要利用UIScrollView
的一些實現UIScrollViewDelegate
協議的委託方法(和其餘相似於UITableView
和UICollectionView
基於滾動視圖的控件同樣):
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate; - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
你可使用這幾個方法來檢測傳送器是否中止滾動,而後加載高分辨率的圖片。只要高分辨率圖片和低分辨率圖片尺寸顏色保持一致,你會很難察覺到替換的過程(確保在同一臺機器使用相同的圖像程序或者腳本生成這些圖片)。
若是有不少張圖片要顯示,最好不要提早把全部都加載進來,而是應該當移出屏幕以後馬上銷燬。經過選擇性的緩存,你就能夠避免來回滾動時圖片重複性的加載了。
緩存其實很簡單:就是存儲昂貴計算後的結果(或者是從閃存或者網絡加載的文件)在內存中,以便後續使用,這樣訪問起來很快。問題在於緩存本質上是一個權衡過程 - 爲了提高性能而消耗了內存,可是因爲內存是一個很是寶貴的資源,因此不能把全部東西都作緩存。
什麼時候將何物作緩存(作多久)並不老是很明顯。幸運的是,大多狀況下,iOS都爲咱們作好了圖片的緩存。
+imageNamed:
方法以前咱們提到使用[UIImage imageNamed:]
加載圖片有個好處在於能夠馬上解壓圖片而不用等到繪製的時候。可是[UIImage imageNamed:]
方法有另外一個很是顯著的好處:它在內存中自動緩存瞭解壓後的圖片,即便你本身沒有保留對它的任何引用。
對於iOS應用那些主要的圖片(例如圖標,按鈕和背景圖片),使用[UIImage imageNamed:]
加載圖片是最簡單最有效的方式。在nib文件中引用的圖片一樣也是這個機制,因此你不少時候都在隱式的使用它。
可是[UIImage imageNamed:]
並不適用任何狀況。它爲用戶界面作了優化,可是並非對應用程序須要顯示的全部類型的圖片都適用。有些時候你仍是要實現本身的緩存機制,緣由以下:
[UIImage imageNamed:]
方法僅僅適用於在應用程序資源束目錄下的圖片,可是大多數應用的許多圖片都要從網絡或者是用戶的相機中獲取,因此[UIImage imageNamed:]
就無法用了。
[UIImage imageNamed:]
緩存用來存儲應用界面的圖片(按鈕,背景等等)。若是對照片這種大圖也用這種緩存,那麼iOS系統就極可能會移除這些圖片來節省內存。那麼在切換頁面時性能就會降低,由於這些圖片都須要從新加載。對傳送器的圖片使用一個單獨的緩存機制就能夠把它和應用圖片的生命週期解耦。
[UIImage imageNamed:]
緩存機制並非公開的,因此你不能很好地控制它。例如,你無法作到檢測圖片是否在加載以前就作了緩存,不可以設置緩存大小,當圖片沒用的時候也不能把它從緩存中移除。
構建一個所謂的緩存系統很是困難。菲爾 卡爾頓曾經說過:「在計算機科學中只有兩件難事:緩存和命名」。
若是要寫本身的圖片緩存的話,那該如何實現呢?讓咱們來看看要涉及哪些方面:
選擇一個合適的緩存鍵 - 緩存鍵用來作圖片的惟一標識。若是實時建立圖片,一般不太好生成一個字符串來區分別的圖片。在咱們的圖片傳送帶例子中就很簡單,咱們能夠用圖片的文件名或者表格索引。
提早緩存 - 若是生成和加載數據的代價很大,你可能想當第一次須要用到的時候再去加載和緩存。提早加載的邏輯是應用內在就有的,可是在咱們的例子中,這也很是好實現,由於對於一個給定的位置和滾動方向,咱們就能夠精確地判斷出哪一張圖片將會出現。
緩存失效 - 若是圖片文件發生了變化,怎樣才能通知到緩存更新呢?這是個很是困難的問題(就像菲爾 卡爾頓提到的),可是幸運的是當從程序資源加載靜態圖片的時候並不須要考慮這些。對用戶提供的圖片來講(可能會被修改或者覆蓋),一個比較好的方式就是當圖片緩存的時候打上一個時間戳以便當文件更新的時候做比較。
緩存回收 - 當內存不夠的時候,如何判斷哪些緩存須要清空呢?這就須要到你寫一個合適的算法了。幸運的是,對緩存回收的問題,蘋果提供了一個叫作NSCache
通用的解決方案
NSCache
和NSDictionary
相似。你能夠經過-setObject:forKey:
和-object:forKey:
方法分別來插入,檢索。和字典不一樣的是,NSCache
在系統低內存的時候自動丟棄存儲的對象。
NSCache
用來判斷什麼時候丟棄對象的算法並無在文檔中給出,可是你可使用-setCountLimit:
方法設置緩存大小,以及-setObject:forKey:cost:
來對每一個存儲的對象指定消耗的值來提供一些暗示。
指定消耗數值能夠用來指定相對的重建成本。若是對大圖指定一個大的消耗值,那麼緩存就知道這些物體的存儲更加昂貴,因而當有大的性能問題的時候纔會丟棄這些物體。你也能夠用-setTotalCostLimit:
方法來指定全體緩存的尺寸。
NSCache
是一個廣泛的緩存解決方案,咱們建立一個比傳送器案例更好的自定義的緩存類。(例如,咱們能夠基於不一樣的緩存圖片索引和當前中間索引來判斷哪些圖片須要首先被釋放)。可是NSCache
對咱們當前的緩存需求來講已經足夠了;不必過早作優化。
使用圖片緩存和提早加載的實現來擴展以前的傳送器案例,而後來看看是否效果更好(見清單14.5)。
清單14.5 添加緩存
#import "ViewController.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:@"png" inDirectory:@"Vacation Photos"]; //register cell class [self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"Cell"]; } - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return [self.imagePaths count]; } - (UIImage *)loadImageAtIndex:(NSUInteger)index { //set up cache static NSCache *cache = nil; if (!cache) { cache = [[NSCache alloc] init]; } //if already cached, return immediately UIImage *image = [cache objectForKey:@(index)]; if (image) { return [image isKindOfClass:[NSNull class]]? nil: image; } //set placeholder to avoid reloading image multiple times [cache setObject:[NSNull null] forKey:@(index)]; //switch to background thread dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ //load image NSString *imagePath = self.imagePaths[index]; UIImage *image = [UIImage imageWithContentsOfFile:imagePath]; //redraw image using device context UIGraphicsBeginImageContextWithOptions(image.size, YES, 0); [image drawAtPoint:CGPointZero]; image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); //set image for correct image view dispatch_async(dispatch_get_main_queue(), ^{ //cache the image [cache setObject:image forKey:@(index)]; //display the image NSIndexPath *indexPath = [NSIndexPath indexPathForItem: index inSection:0]; UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath]; UIImageView *imageView = [cell.contentView.subviews lastObject]; imageView.image = image; }); }); //not loaded yet return nil; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { //dequeue cell UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath]; //add image view UIImageView *imageView = [cell.contentView.subviews lastObject]; if (!imageView) { imageView = [[UIImageView alloc] initWithFrame:cell.contentView.bounds]; imageView.contentMode = UIViewContentModeScaleAspectFit; [cell.contentView addSubview:imageView]; } //set or load image for this index imageView.image = [self loadImageAtIndex:indexPath.item]; //preload image for previous and next index if (indexPath.item < [self.imagePaths count] - 1) { [self loadImageAtIndex:indexPath.item + 1]; } if (indexPath.item > 0) { [self loadImageAtIndex:indexPath.item - 1]; } return cell; } @end
果真效果更好了!當滾動的時候雖然還有一些圖片進入的延遲,可是已經很是罕見了。緩存意味着咱們作了更少的加載。這裏提早加載邏輯很是粗暴,其實能夠把滑動速度和方向也考慮進來,但這已經比以前沒作緩存的版本好不少了。
圖片加載性能取決於加載大圖的時間和解壓小圖時間的權衡。不少蘋果的文檔都說PNG是iOS全部圖片加載的最好格式。但這是極度誤導的過期信息了。
PNG圖片使用的無損壓縮算法能夠比使用JPEG的圖片作到更快地解壓,可是因爲閃存訪問的緣由,這些加載的時間並無什麼區別。
清單14.6展現了標準的應用程序加載不一樣尺寸圖片所須要時間的一些代碼。爲了保證明驗的準確性,咱們會測量每張圖片的加載和繪製時間來確保考慮到解壓性能的因素。另外每隔一秒重複加載和繪製圖片,這樣就能夠取到平均時間,使得結果更加準確。
清單14.6
#import "ViewController.h" static NSString *const ImageFolder = @"Coast Photos"; @interface ViewController () <UITableViewDataSource> @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: %03ims JPG: %03ims", 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 不一樣類型圖片的相對加載性能
如結果所示,相對於不友好的PNG圖片,相同像素的JPEG圖片老是比PNG加載更快,除非一些很是小的圖片、但對於友好的PNG圖片,一些中大尺寸的圖效果仍是很好的。
因此對於以前的圖片傳送器程序來講,JPEG會是個不錯的選擇。若是用JPEG的話,一些多線程和緩存策略都不必了。
但JPEG圖片並非全部狀況都適用。若是圖片須要一些透明效果,或者壓縮以後細節損耗不少,那就該考慮用別的格式了。蘋果在iOS系統中對PNG和JPEG都作了一些優化,因此普通狀況下都應該用這種格式。也就是說在一些特殊的狀況下才應該使用別的格式。
對於包含透明的圖片來講,最好是使用壓縮透明通道的PNG圖片和壓縮RGB部分的JPEG圖片混合起來加載。這就對任何格式都適用了,並且不管從質量仍是文件尺寸仍是加載性能來講都和PNG和JPEG的圖片相近。相關分別加載顏色和遮罩圖片並在運行時合成的代碼見14.7。
清單14.7 從PNG遮罩和JPEG建立的混合圖片
#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和PNG以外iOS還支持別的一些格式,例如TIFF和GIF,可是因爲他們質量壓縮得更厲害,性能比JPEG和PNG糟糕的多,因此大多數狀況並不用考慮。
可是iOS以後,蘋果低調添加了對JPEG 2000圖片格式的支持,因此大多數人並不知道。它甚至並不被Xcode很好的支持 - JPEG 2000圖片都沒在Interface Builder中顯示。
可是JPEG 2000圖片在(設備和模擬器)運行時會有效,並且比JPEG質量更好,一樣也對透明通道有很好的支持。可是JPEG 2000圖片在加載和顯示圖片方面明顯要比PNG和JPEG慢得多,因此對圖片大小比運行效率更敏感的時候,使用它是一個不錯的選擇。
但仍然要對JPEG 2000保持關注,由於在後續iOS版本說不定就對它的性能作提高,可是在現階段,混合圖片對更小尺寸和質量的文件性能會更好。
當前市場的每一個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圖片
#import "ViewController.h" #import <QuartzCore/QuartzCore.h> #import <GLKit/GLKit.h> @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的遊戲),能夠參考一下的庫(https://github.com/nicklockwood/GLView),它提供了一個簡單的類,從新實現了的各類功能,但同時提供了PVRTC圖片,而不須要你寫任何OpenGL代碼。GLViewGLImageViewUIImageView
在這章中,咱們研究了和圖片加載解壓相關的性能問題,並延展了一系列解決方案。
在第15章「圖層性能」中,咱們將討論和圖層渲染和組合相關的性能問題。