????如果有很多張圖片要顯示,最好不要提前把所有都加載進(jìn)來,而是應(yīng)該當(dāng)移出屏幕之后立刻銷毀。通過選擇性的緩存,你就可以避免來回滾動時(shí)圖片重復(fù)性的加載了。
????緩存其實(shí)很簡單:就是存儲昂貴計(jì)算后的結(jié)果(或者是從閃存或者網(wǎng)絡(luò)加載的文件)在內(nèi)存中,以便后續(xù)使用,這樣訪問起來很快。問題在于緩存本質(zhì)上是一個(gè)權(quán)衡過程 - 為了提升性能而消耗了內(nèi)存,但是由于內(nèi)存是一個(gè)非常寶貴的資源,所以不能把所有東西都做緩存。
????何時(shí)將何物做緩存(做多久)并不總是很明顯。幸運(yùn)的是,大多情況下,iOS都為我們做好了圖片的緩存。
+imageNamed:
方法????之前我們提到使用[UIImage imageNamed:]
加載圖片有個(gè)好處在于可以立刻解壓圖片而不用等到繪制的時(shí)候。但是[UIImage imageNamed:]
方法有另一個(gè)非常顯著的好處:它在內(nèi)存中自動緩存了解壓后的圖片,即使你自己沒有保留對它的任何引用。
????對于iOS應(yīng)用那些主要的圖片(例如圖標(biāo),按鈕和背景圖片),使用[UIImage imageNamed:]
加載圖片是最簡單最有效的方式。在nib文件中引用的圖片同樣也是這個(gè)機(jī)制,所以你很多時(shí)候都在隱式的使用它。
????但是[UIImage imageNamed:]
并不適用任何情況。它為用戶界面做了優(yōu)化,但是并不是對應(yīng)用程序需要顯示的所有類型的圖片都適用。有些時(shí)候你還是要實(shí)現(xiàn)自己的緩存機(jī)制,原因如下:
[UIImage imageNamed:]
方法僅僅適用于在應(yīng)用程序資源束目錄下的圖片,但是大多數(shù)應(yīng)用的許多圖片都要從網(wǎng)絡(luò)或者是用戶的相機(jī)中獲取,所以[UIImage imageNamed:]
就沒法用了。
[UIImage imageNamed:]
緩存用來存儲應(yīng)用界面的圖片(按鈕,背景等等)。如果對照片這種大圖也用這種緩存,那么iOS系統(tǒng)就很可能會移除這些圖片來節(jié)省內(nèi)存。那么在切換頁面時(shí)性能就會下降,因?yàn)檫@些圖片都需要重新加載。對傳送器的圖片使用一個(gè)單獨(dú)的緩存機(jī)制就可以把它和應(yīng)用圖片的生命周期解耦。
[UIImage imageNamed:]
緩存機(jī)制并不是公開的,所以你不能很好地控制它。例如,你沒法做到檢測圖片是否在加載之前就做了緩存,不能夠設(shè)置緩存大小,當(dāng)圖片沒用的時(shí)候也不能把它從緩存中移除。????構(gòu)建一個(gè)所謂的緩存系統(tǒng)非常困難。菲爾 卡爾頓曾經(jīng)說過:“在計(jì)算機(jī)科學(xué)中只有兩件難事:緩存和命名”。
????如果要寫自己的圖片緩存的話,那該如何實(shí)現(xiàn)呢?讓我們來看看要涉及哪些方面:
選擇一個(gè)合適的緩存鍵 - 緩存鍵用來做圖片的唯一標(biāo)識。如果實(shí)時(shí)創(chuàng)建圖片,通常不太好生成一個(gè)字符串來區(qū)分別的圖片。在我們的圖片傳送帶例子中就很簡單,我們可以用圖片的文件名或者表格索引。
提前緩存 - 如果生成和加載數(shù)據(jù)的代價(jià)很大,你可能想當(dāng)?shù)谝淮涡枰玫降臅r(shí)候再去加載和緩存。提前加載的邏輯是應(yīng)用內(nèi)在就有的,但是在我們的例子中,這也非常好實(shí)現(xiàn),因?yàn)閷τ谝粋€(gè)給定的位置和滾動方向,我們就可以精確地判斷出哪一張圖片將會出現(xiàn)。
緩存失效 - 如果圖片文件發(fā)生了變化,怎樣才能通知到緩存更新呢?這是個(gè)非常困難的問題(就像菲爾 卡爾頓提到的),但是幸運(yùn)的是當(dāng)從程序資源加載靜態(tài)圖片的時(shí)候并不需要考慮這些。對用戶提供的圖片來說(可能會被修改或者覆蓋),一個(gè)比較好的方式就是當(dāng)圖片緩存的時(shí)候打上一個(gè)時(shí)間戳以便當(dāng)文件更新的時(shí)候作比較。
NSCache
通用的解決方案????NSCache
和NSDictionary
類似。你可以通過-setObject:forKey:
和-object:forKey:
方法分別來插入,檢索。和字典不同的是,NSCache
在系統(tǒng)低內(nèi)存的時(shí)候自動丟棄存儲的對象。
????NSCache
用來判斷何時(shí)丟棄對象的算法并沒有在文檔中給出,但是你可以使用-setCountLimit:
方法設(shè)置緩存大小,以及-setObject:forKey:cost:
來對每個(gè)存儲的對象指定消耗的值來提供一些暗示。
????指定消耗數(shù)值可以用來指定相對的重建成本。如果對大圖指定一個(gè)大的消耗值,那么緩存就知道這些物體的存儲更加昂貴,于是當(dāng)有大的性能問題的時(shí)候才會丟棄這些物體。你也可以用-setTotalCostLimit:
方法來指定全體緩存的尺寸。
????NSCache
是一個(gè)普遍的緩存解決方案,我們創(chuàng)建一個(gè)比傳送器案例更好的自定義的緩存類。(例如,我們可以基于不同的緩存圖片索引和當(dāng)前中間索引來判斷哪些圖片需要首先被釋放)。但是NSCache
對我們當(dāng)前的緩存需求來說已經(jīng)足夠了;沒必要過早做優(yōu)化。
????使用圖片緩存和提前加載的實(shí)現(xiàn)來擴(kuò)展之前的傳送器案例,然后來看看是否效果更好(見清單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
????果然效果更好了!當(dāng)滾動的時(shí)候雖然還有一些圖片進(jìn)入的延遲,但是已經(jīng)非常罕見了。緩存意味著我們做了更少的加載。這里提前加載邏輯非常粗暴,其實(shí)可以把滑動速度和方向也考慮進(jìn)來,但這已經(jīng)比之前沒做緩存的版本好很多了。
更多建議: