posts - 10,  comments - 1,  trackbacks - 0

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提供了一些選擇: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章“圖層性能”中,我們將討論和圖層渲染和組合相關的性能問題。

posted on 2019-12-10 13:30  Julday  閱讀(...)  評論(...編輯  收藏

四川金7乐历史开奖号码查询