13. 高效繪圖
高效繪圖
不必要的效率考慮往往是性能問題的萬惡之源, ——William Allan Wulf
在第12章『速度的曲率』我們學習如何用Instruments來診斷Core Animation性能問題,在構建一個iOS app的時候會遇到很多潛在的性能陷阱,但是在本章我們將著眼于有關繪制的性能問題,
13.1 軟體繪圖
軟體繪圖
術語繪圖通常在Core Animation的背景關系中指代軟體繪圖(意即:不由GPU協助的繪圖),在iOS中,軟體繪圖通常是由Core Graphics框架完成來完成,但是,在一些必要的情況下,相比Core Animation和OpenGL,Core Graphics要慢了不少,
軟體繪圖不僅效率低,還會消耗可觀的記憶體,CALayer只需要一些與自己相關的記憶體:只有它的寄宿圖會消耗一定的記憶體空間,即使直接賦給contents屬性一張圖片,也不需要增加額外的照片存盤大小,如果相同的一張圖片被多個圖層作為contents屬性,那么他們將會共用同一塊記憶體,而不是復制記憶體塊,
一個開發者,有一個學習的氛圍跟一個交流圈子特別重要,這是一個我的iOS交流群:1012951431, 分享BAT,阿里面試題、面試經驗,討論技術, 大家一起交流學習成長!希望幫助開發者少走彎路,
但是一旦你實作了CALayerDelegate協議中的-drawLayer:inContext:方法或者UIView中的-drawRect:方法(其實就是前者的包裝方法),圖層就創建了一個繪制背景關系,這個背景關系需要的大小的記憶體可從這個算式得出:圖層寬圖層高4位元組,寬高的單位均為像素,對于一個在Retina iPad上的全屏圖層來說,這個記憶體量就是 2048 15264位元組,相當于12MB記憶體,圖層每次重繪的時候都需要重新抹掉記憶體然后重新分配,
軟體繪圖的代價昂貴,除非絕對必要,你應該避免重繪你的視圖,提高繪制性能的秘訣就在于盡量避免去繪制,
13.2 矢量圖形
矢量圖形
我們用Core Graphics來繪圖的一個通常原因就是只是用圖片或是圖層效果不能輕易地繪制出矢量圖形,矢量繪圖包含一下這些:
-
任意多邊形(不僅僅是一個矩形)
-
斜線或曲線
-
文本
-
漸變
舉個例子,清單13.1 展示了一個基本的畫線應用,這個應用將用戶的觸摸手勢轉換成一個UIBezierPath上的點,然后繪制成視圖,我們在一個UIView子類DrawingView中實作了所有的繪制邏輯,這個情況下我們沒有用上view controller,但是如果你喜歡你可以在view controller中實作觸摸事件處理,圖13.1是代碼運行結果,
清單13.1 用Core Graphics實作一個簡單的繪圖應用
#import "DrawingView.h" @interface DrawingView () @property (nonatomic, strong) UIBezierPath *path; @end @implementation DrawingView - (void)awakeFromNib { //create a mutable path self.path = [[UIBezierPath alloc] init]; self.path.lineJoinStyle = kCGLineJoinRound; self.path.lineCapStyle = kCGLineCapRound; ? self.path.lineWidth = 5; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { //get the starting point CGPoint point = [[touches anyObject] locationInView:self]; //move the path drawing cursor to the starting point [self.path moveToPoint:point]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { //get the current point CGPoint point = [[touches anyObject] locationInView:self]; //add a new line segment to our path [self.path addLineToPoint:point]; //redraw the view [self setNeedsDisplay]; } - (void)drawRect:(CGRect)rect { //draw path [[UIColor clearColor] setFill]; [[UIColor redColor] setStroke]; [self.path stroke]; } @end
13.3 臟矩形
臟矩形
有時候用CAShapeLayer或者其他矢量圖形圖層替代Core Graphics并不是那么切實可行,比如我們的繪圖應用:我們用線條完美地完成了矢量繪制,但是設想一下如果我們能進一步提高應用的性能,讓它就像一個黑板一樣作業,然后用『粉筆』來繪制線條,模擬粉筆最簡單的方法就是用一個『線刷』圖片然后將它粘貼到用戶手指碰觸的地方,但是這個方法用CAShapeLayer沒辦法實作,
我們可以給每個『線刷』創建一個獨立的圖層,但是實作起來有很大的問題,螢屏上允許同時出現圖層上線數量大約是幾百,那樣我們很快就會超出的,這種情況下我們沒什么辦法,就用Core Graphics吧(除非你想用OpenGL做一些更復雜的事情),
我們的『黑板』應用的最初實作見清單13.3,我們更改了之前版本的DrawingView,用一個畫刷位置的陣列代替UIBezierPath,圖13.2是運行結果
清單13.3 簡單的類似黑板的應用
#import "DrawingView.h" #import #define BRUSH_SIZE 32 @interface DrawingView () @property (nonatomic, strong) NSMutableArray *strokes; @end @implementation DrawingView - (void)awakeFromNib { //create array self.strokes = [NSMutableArray array]; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { //get the starting point CGPoint point = [[touches anyObject] locationInView:self]; //add brush stroke [self addBrushStrokeAtPoint:point]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { //get the touch point CGPoint point = [[touches anyObject] locationInView:self]; //add brush stroke [self addBrushStrokeAtPoint:point]; } - (void)addBrushStrokeAtPoint:(CGPoint)point { //add brush stroke to array [self.strokes addObject:[NSValue valueWithCGPoint:point]]; //needs redraw [self setNeedsDisplay]; } - (void)drawRect:(CGRect)rect { //redraw strokes for (NSValue *value in self.strokes) { //get point CGPoint point = [value CGPointValue]; //get brush rect CGRect brushRect = CGRectMake(point.x - BRUSH_SIZE/2, point.y - BRUSH_SIZE/2, BRUSH_SIZE, BRUSH_SIZE); //draw brush stroke ? [[UIImage imageNamed:@"Chalk.png"] drawInRect:brushRect]; } } @end
圖13.3 幀率和線條質量會隨時間下降,
為了減少不必要的繪制,Mac OS和iOS設備將會把螢屏區分為需要重繪的區域和不需要重繪的區域,那些需要重繪的部分被稱作『臟區域』,在實際應用中,鑒于非矩形區域邊界裁剪和混合的復雜性,通常會區分出包含指定視圖的矩形位置,而這個位置就是『臟矩形』,
當一個視圖被改動過了,TA可能需要重繪,但是很多情況下,只是這個視圖的一部分被改變了,所以重繪整個寄宿圖就太浪費了,但是Core Animation通常并不了解你的自定義繪圖代碼,它也不能自己計算出臟區域的位置,然而,你的確可以提供這些資訊,
當你檢測到指定視圖或圖層的指定部分需要被重繪,你直接呼叫-setNeedsDisplayInRect:來標記它,然后將影響到的矩形作為引數傳入,這樣就會在一次視圖重繪時呼叫視圖的-drawRect:(或圖層代理的-drawLayer:inContext:方法),
傳入-drawLayer:inContext:的CGContext引數會自動被裁切以適應對應的矩形,為了確定矩形的尺寸大小,你可以用CGContextGetClipBoundingBox()方法來從背景關系獲得大小,呼叫-drawRect()會更簡單,因為CGRect會作為引數直接傳入,
你應該將你的繪制作業限制在這個矩形中,任何在此區域之外的繪制都將被自動無視,但是這樣CPU花在計算和拋棄上的時間就浪費了,實在是太不值得了,
相比依賴于Core Graphics為你重繪,裁剪出自己的繪制區域可能會讓你避免不必要的操作,那就是說,如果你的裁剪邏輯相當復雜,那還是讓Core Graphics來代勞吧,記住:當你能高效完成的時候才這樣做,
清單13.4 展示了一個-addBrushStrokeAtPoint:方法的升級版,它只重繪當前線刷的附近區域,另外也會重繪之前線刷的附近區域,我們也可以用CGRectIntersectsRect()來避免重繪任何舊的線刷以不至于覆寫已更新過的區域,這樣做會顯著地提高繪制效率(見圖13.4)
清單13.4 用-setNeedsDisplayInRect:來減少不必要的繪制
- (void)addBrushStrokeAtPoint:(CGPoint)point { //add brush stroke to array [self.strokes addObject:[NSValue valueWithCGPoint:point]]; //set dirty rect [self setNeedsDisplayInRect:[self brushRectForPoint:point]]; } - (CGRect)brushRectForPoint:(CGPoint)point { return CGRectMake(point.x - BRUSH_SIZE/2, point.y - BRUSH_SIZE/2, BRUSH_SIZE, BRUSH_SIZE); } - (void)drawRect:(CGRect)rect { //redraw strokes for (NSValue *value in self.strokes) { //get point CGPoint point = [value CGPointValue]; //get brush rect CGRect brushRect = [self brushRectForPoint:point]; ? //only draw brush stroke if it intersects dirty rect if (CGRectIntersectsRect(rect, brushRect)) { //draw brush stroke [[UIImage imageNamed:@"Chalk.png"] drawInRect:brushRect]; } } }
13.4 異步繪制
異步繪制
UIKit的單執行緒天性意味著寄宿圖通暢要在主執行緒上更新,這意味著繪制會打斷用戶互動,甚至讓整個app看起來處于無回應狀態,我們對此無能為力,但是如果能避免用戶等待繪制完成就好多了,
針對這個問題,有一些方法可以用到:一些情況下,我們可以推測性地提前在另外一個執行緒上繪制內容,然后將由此繪出的圖片直接設定為圖層的內容,這實作起來可能不是很方便,但是在特定情況下是可行的,Core Animation提供了一些選擇:CATiledLayer和drawsAsynchronously屬性,
CATiledLayer
我們在第六章簡單探索了一下CATiledLayer,除了將圖層再次分割成獨立更新的小塊(類似于臟矩形自動更新的概念),CATiledLayer還有一個有趣的特性:在多個執行緒中為每個小塊同時呼叫-drawLayer:inContext:方法,這就避免了阻塞用戶互動而且能夠利用多核心新片來更快地繪制,只有一個小塊的CATiledLayer是實作異步更新圖片視圖的簡單方法,
drawsAsynchronously
iOS 6中,蘋果為CALayer引入了這個令人好奇的屬性,drawsAsynchronously屬性對傳入-drawLayer:inContext:的CGContext進行改動,允許CGContext延緩繪制命令的執行以至于不阻塞用戶互動,
它與CATiledLayer使用的異步繪制并不相同,它自己的-drawLayer:inContext:方法只會在主執行緒呼叫,但是CGContext并不等待每個繪制命令的結束,相反地,它會將命令加入佇列,當方法回傳時,在后臺執行緒逐個執行真正的繪制,
根據蘋果的說法,這個特性在需要頻繁重繪的視圖上效果最好(比如我們的繪圖應用,或者諸如UITableViewCell之類的),對那些只繪制一次或很少重繪的圖層內容來說沒什么太大的幫助,
13.5 總結
總結
本章我們主要圍繞用Core Graphics軟體繪制討論了一些性能挑戰,然后探索了一些改進方法:比如提高繪制性能或者減少需要繪制的數量,第14章,『影像IO』,我們將討論圖片的載入性能,
14. 影像IO
影像IO
潛伏期值得思考 - 凱文 帕薩特
在第13章“高效繪圖”中,我們研究了和Core Graphics繪圖相關的性能問題,以及如何修復,和繪圖性能相關緊密相關的是影像性能,在這一章中,我們將研究如何優化從閃存驅動器或者網路中加載和顯示圖片,
14.1 加載和潛伏
加載和潛伏
繪圖實際消耗的時間通常并不是影響性能的因素,圖片消耗很大一部分記憶體,而且不太可能把需要顯示的圖片都保留在記憶體中,所以需要在應用運行的時候周期性地加載和卸載圖片,
圖片檔案加載的速度被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() @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.2 時間分析工具展示了CPU瓶頸
這里提升性能唯一的方式就是在另一個執行緒中加載圖片,這并不能夠降低實際的加載時間(可能情況會更糟,因為系統可能要消耗CPU時間來處理加載的圖片資料),但是主執行緒能夠有時間做一些別的事情,比如回應用戶輸入,以及滑動影片,
為了在后臺執行緒加載圖片,我們可以使用GCD或者NSOperationQueue創建自定義執行緒,或者使用CATiledLayer,為了從遠程網路加載圖片,我們可以使用異步的NSURLConnection,但是對本地存盤的圖片,并不十分有效,
GCD和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.2 快取
快取
如果有很多張圖片要顯示,最好不要提前把所有都加載進來,而是應該當移出螢屏之后立刻銷毀,通過選擇性的快取,你就可以避免來回滾動時圖片重復性的加載了,
快取其實很簡單:就是存盤昂貴計算后的結果(或者是從閃存或者網路加載的檔案)在記憶體中,以便后續使用,這樣訪問起來很快,問題在于快取本質上是一個權衡程序 - 為了提升性能而消耗了記憶體,但是由于記憶體是一個非常寶貴的資源,所以不能把所有東西都做快取,
何時將何物做快取(做多久)并不總是很明顯,幸運的是,大多情況下,iOS都為我們做好了圖片的快取,
+imageNamed:方法
之前我們提到使用[UIImage imageNamed:]加載圖片有個好處在于可以立刻解壓圖片而不用等到繪制的時候,但是[UIImage imageNamed:]方法有另一個非常顯著的好處:它在記憶體中自動快取了解壓后的圖片,即使你自己沒有保留對它的任何參考,
對于iOS應用那些主要的圖片(例如圖示,按鈕和背景圖片),使用[UIImage imageNamed:]加載圖片是最簡單最有效的方式,在nib檔案中參考的圖片同樣也是這個機制,所以你很多時候都在隱式的使用它,
但是[UIImage imageNamed:]并不適用任何情況,它為用戶界面做了優化,但是并不是對應用程式需要顯示的所有型別的圖片都適用,有些時候你還是要實作自己的快取機制,原因如下:
-
[UIImage imageNamed:]方法僅僅適用于在應用程式資源束目錄下的圖片,但是大多數應用的許多圖片都要從網路或者是用戶的相機中獲取,所以[UIImage imageNamed:]就沒法用了, -
[UIImage imageNamed:]快取用來存盤應用界面的圖片(按鈕,背景等等),如果對照片這種大圖也用這種快取,那么iOS系統就很可能會移除這些圖片來節省記憶體,那么在切換頁面時性能就會下降,因為這些圖片都需要重新加載,對傳送器的圖片使用一個單獨的快取機制就可以把它和應用圖片的生命周期解耦, -
[UIImage imageNamed:]快取機制并不是公開的,所以你不能很好地控制它,例如,你沒法做到檢測圖片是否在加載之前就做了快取,不能夠設定快取大小,當圖片沒用的時候也不能把它從快取中移除,
自定義快取
構建一個所謂的快取系統非常困難,菲爾 卡爾頓曾經說過:“在計算機科學中只有兩件難事:快取和命名”,
如果要寫自己的圖片快取的話,那該如何實作呢?讓我們來看看要涉及哪些方面:
-
選擇一個合適的快取鍵 - 快取鍵用來做圖片的唯一標識,如果實時創建圖片,通常不太好生成一個字串來區分別的圖片,在我們的圖片傳送帶例子中就很簡單,我們可以用圖片的檔案名或者表格索引,
-
提前快取 - 如果生成和加載資料的代價很大,你可能想當第一次需要用到的時候再去加載和快取,提前加載的邏輯是應用內在就有的,但是在我們的例子中,這也非常好實作,因為對于一個給定的位置和滾動方向,我們就可以精確地判斷出哪一張圖片將會出現,
-
快取失效 - 如果圖片檔案發生了變化,怎樣才能通知到快取更新呢?這是個非常困難的問題(就像菲爾 卡爾頓提到的),但是幸運的是當從程式資源加載靜態圖片的時候并不需要考慮這些,對用戶提供的圖片來說(可能會被修改或者覆寫),一個比較好的方式就是當圖片快取的時候打上一個時間戳以便當檔案更新的時候作比較,
-
快取回收 - 當記憶體不夠的時候,如何判斷哪些快取需要清空呢?這就需要到你寫一個合適的演算法了,幸運的是,對快取回收的問題,蘋果提供了一個叫做
NSCache通用的解決方案
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() @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
果然效果更好了!當滾動的時候雖然還有一些圖片進入的延遲,但是已經非常罕見了,快取意味著我們做了更少的加載,這里提前加載邏輯非常粗暴,其實可以把滑動速度和方向也考慮進來,但這已經比之前沒做快取的版本好很多了,
14.3 檔案格式
檔案格式
圖片加載性能取決于加載大圖的時間和解壓小圖時間的權衡,很多蘋果的檔案都說PNG是iOS所有圖片加載的最好格式,但這是極度誤導的過時資訊了,
PNG圖片使用的無損壓縮演算法可以比使用JPEG的圖片做到更快地解壓,但是由于閃存訪問的原因,這些加載的時間并沒有什么區別,
清單14.6展示了標準的應用程式加載不同尺寸圖片所需要時間的一些代碼,為了保證實驗的準確性,我們會測量每張圖片的加載和繪制時間來確保考慮到解壓性能的因素,另外每隔一秒重復加載和繪制圖片,這樣就可以取到平均時間,使得結果更加準確,
清單14.6
#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: %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.4 總結
總結
在這章中,我們研究了和圖片加載解壓相關的性能問題,并延展了一系列解決方案,
在第15章“圖層性能”中,我們將討論和圖層渲染和組合相關的性能問題,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/5581.html
標籤:iOS
上一篇:深入了解一些Android影片
下一篇:iOS:bugly符號表上傳
