14.2 緩存

2018-02-24 15:07 更新

緩存

????如果有很多張圖片要顯示,最好不要提前把所有都加載進來,而是應該當移出屏幕之后立刻銷毀。通過選擇性的緩存,你就可以避免來回滾動時圖片重復性的加載了。

????緩存其實很簡單:就是存儲昂貴計算后的結果(或者是從閃存或者網絡加載的文件)在內存中,以便后續(xù)使用,這樣訪問起來很快。問題在于緩存本質上是一個權衡過程 - 為了提升性能而消耗了內存,但是由于內存是一個非常寶貴的資源,所以不能把所有東西都做緩存。

????何時將何物做緩存(做多久)并不總是很明顯。幸運的是,大多情況下,iOS都為我們做好了圖片的緩存。

+imageNamed:方法

????之前我們提到使用[UIImage imageNamed:]加載圖片有個好處在于可以立刻解壓圖片而不用等到繪制的時候。但是[UIImage imageNamed:]方法有另一個非常顯著的好處:它在內存中自動緩存了解壓后的圖片,即使你自己沒有保留對它的任何引用。

????對于iOS應用那些主要的圖片(例如圖標,按鈕和背景圖片),使用[UIImage imageNamed:]加載圖片是最簡單最有效的方式。在nib文件中引用的圖片同樣也是這個機制,所以你很多時候都在隱式的使用它。

????但是[UIImage imageNamed:]并不適用任何情況。它為用戶界面做了優(yōu)化,但是并不是對應用程序需要顯示的所有類型的圖片都適用。有些時候你還是要實現自己的緩存機制,原因如下:

  • [UIImage imageNamed:]方法僅僅適用于在應用程序資源束目錄下的圖片,但是大多數應用的許多圖片都要從網絡或者是用戶的相機中獲取,所以[UIImage imageNamed:]就沒法用了。

  • [UIImage imageNamed:]緩存用來存儲應用界面的圖片(按鈕,背景等等)。如果對照片這種大圖也用這種緩存,那么iOS系統(tǒng)就很可能會移除這些圖片來節(jié)省內存。那么在切換頁面時性能就會下降,因為這些圖片都需要重新加載。對傳送器的圖片使用一個單獨的緩存機制就可以把它和應用圖片的生命周期解耦。

  • [UIImage imageNamed:]緩存機制并不是公開的,所以你不能很好地控制它。例如,你沒法做到檢測圖片是否在加載之前就做了緩存,不能夠設置緩存大小,當圖片沒用的時候也不能把它從緩存中移除。

自定義緩存

????構建一個所謂的緩存系統(tǒng)非常困難。菲爾 卡爾頓曾經說過:“在計算機科學中只有兩件難事:緩存和命名”。

????如果要寫自己的圖片緩存的話,那該如何實現呢?讓我們來看看要涉及哪些方面:

  • 選擇一個合適的緩存鍵 - 緩存鍵用來做圖片的唯一標識。如果實時創(chuàng)建圖片,通常不太好生成一個字符串來區(qū)分別的圖片。在我們的圖片傳送帶例子中就很簡單,我們可以用圖片的文件名或者表格索引。

  • 提前緩存 - 如果生成和加載數據的代價很大,你可能想當第一次需要用到的時候再去加載和緩存。提前加載的邏輯是應用內在就有的,但是在我們的例子中,這也非常好實現,因為對于一個給定的位置和滾動方向,我們就可以精確地判斷出哪一張圖片將會出現。

  • 緩存失效 - 如果圖片文件發(fā)生了變化,怎樣才能通知到緩存更新呢?這是個非常困難的問題(就像菲爾 卡爾頓提到的),但是幸運的是當從程序資源加載靜態(tài)圖片的時候并不需要考慮這些。對用戶提供的圖片來說(可能會被修改或者覆蓋),一個比較好的方式就是當圖片緩存的時候打上一個時間戳以便當文件更新的時候作比較。

  • 緩存回收 - 當內存不夠的時候,如何判斷哪些緩存需要清空呢?這就需要到你寫一個合適的算法了。幸運的是,對緩存回收的問題,蘋果提供了一個叫做NSCache通用的解決方案

NSCache

????NSCacheNSDictionary類似。你可以通過-setObject:forKey:-object:forKey:方法分別來插入,檢索。和字典不同的是,NSCache在系統(tǒng)低內存的時候自動丟棄存儲的對象。

????NSCache用來判斷何時丟棄對象的算法并沒有在文檔中給出,但是你可以使用-setCountLimit:方法設置緩存大小,以及-setObject:forKey:cost:來對每個存儲的對象指定消耗的值來提供一些暗示。

????指定消耗數值可以用來指定相對的重建成本。如果對大圖指定一個大的消耗值,那么緩存就知道這些物體的存儲更加昂貴,于是當有大的性能問題的時候才會丟棄這些物體。你也可以用-setTotalCostLimit:方法來指定全體緩存的尺寸。

????NSCache是一個普遍的緩存解決方案,我們創(chuàng)建一個比傳送器案例更好的自定義的緩存類。(例如,我們可以基于不同的緩存圖片索引和當前中間索引來判斷哪些圖片需要首先被釋放)。但是NSCache對我們當前的緩存需求來說已經足夠了;沒必要過早做優(yōu)化。

????使用圖片緩存和提前加載的實現來擴展之前的傳送器案例,然后來看看是否效果更好(見清單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

????果然效果更好了!當滾動的時候雖然還有一些圖片進入的延遲,但是已經非常罕見了。緩存意味著我們做了更少的加載。這里提前加載邏輯非常粗暴,其實可以把滑動速度和方向也考慮進來,但這已經比之前沒做緩存的版本好很多了。

以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號