iOS核心動畫高級技巧 - 7

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來繪圖的一個一般緣由就是隻是用圖片或是圖層效果不能輕易地繪製出矢量圖形。矢量繪圖包含一下這些:app

  • 任意多邊形(不只僅是一個矩形)框架

  • 斜線或曲線異步

  • 文本

  • 漸變

舉個例子,清單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提供了一些選擇:CATiledLayerdrawsAsynchronously屬性。

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

NSCacheNSDictionary相似。你能夠經過-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章「圖層性能」中,咱們將討論和圖層渲染和組合相關的性能問題。

相關文章
相關標籤/搜索