CoreAnimation圖像IO

在第13章“高效繪圖”中,我們研究了和Core Graphics繪圖相關(guān)的性能問題,以及如何修復(fù)。和繪圖性能相關(guān)緊密相關(guān)的是圖像性能。在這一章中,我們將研究如何優(yōu)化從閃存驅(qū)動(dòng)器或者網(wǎng)絡(luò)中加載和顯示圖片。

加載和潛伏

繪圖實(shí)際消耗的時(shí)間通常并不是影響性能的因素。圖片消耗很大一部分內(nèi)存,而且不太可能把需要顯示的圖片都保留在內(nèi)存中,所以需要在應(yīng)用運(yùn)行的時(shí)候周期性地加載和卸載圖片。

圖片文件加載的速度被CPU和IO(輸入/輸出)同時(shí)影響。iOS設(shè)備中的閃存已經(jīng)比傳統(tǒng)硬盤快很多了,但仍然比RAM慢將近200倍左右,這就需要很小心地管理加載,來避免延遲。

只要有可能,試著在程序生命周期不易察覺的時(shí)候來加載圖片,例如啟動(dòng),或者在屏幕切換的過程中。按下按鈕和按鈕響應(yīng)事件之間最大的延遲大概是200ms,這比動(dòng)畫每一幀切換的16ms小得多。你可以在程序首次啟動(dòng)的時(shí)候加載圖片,但是如果20秒內(nèi)無法啟動(dòng)程序的話,iOS檢測計(jì)時(shí)器就會(huì)終止你的應(yīng)用(而且如果啟動(dòng)大于2,3秒的話用戶就會(huì)抱怨了)。

有些時(shí)候,提前加載所有的東西并不明智。比如說包含上千張圖片的圖片傳送帶:用戶希望能夠能夠平滑快速翻動(dòng)圖片,所以就不可能提前預(yù)加載所有圖片;那樣會(huì)消耗太多的時(shí)間和內(nèi)存。

有時(shí)候圖片也需要從遠(yuǎn)程網(wǎng)絡(luò)連接中下載,這將會(huì)比從磁盤加載要消耗更多的時(shí)間,甚至可能由于連接問題而加載失?。ㄔ趲酌腌妵L試之后)。你不能夠在主線程中加載網(wǎng)絡(luò)造成等待,所以需要后臺(tái)線程。

線程加載

在第12章“性能調(diào)優(yōu)”我們的聯(lián)系人列表例子中,圖片都非常小,所以可以在主線程同步加載。但是對(duì)于大圖來說,這樣做就不太合適了,因?yàn)榧虞d會(huì)消耗很長時(shí)間,造成滑動(dòng)的不流暢。滑動(dòng)動(dòng)畫會(huì)在主線程的run

loop中更新,所以會(huì)有更多運(yùn)行在渲染服務(wù)進(jìn)程中CPU相關(guān)的性能問題。

清單14.1顯示了一個(gè)通過UICollectionView實(shí)現(xiàn)的基礎(chǔ)的圖片傳送器。圖片在主線程中-collectionView:cellForItemAtIndexPath:方法中同步加載(見圖14.1)。

清單14.1 使用UICollectionView實(shí)現(xiàn)的圖片傳送器

#import"ViewController.h"

@interfaceViewController() @property(nonatomic,copy)NSArray*imagePaths;

@property(nonatomic,weak)IBOutletUICollectionView *collectionView;

@end

@implementationViewController

- (void)viewDidLoad{

//set up data

self.imagePaths=?? [[NSBundlemainBundle]pathsForResourcesOfType:@"png"inDirectory:@"Vacation Photos"];

//register cell class[self.collectionViewregisterClass:[UICollectionViewCellclass]forCellWithReuseIdentifier:@"Cell"];

}- (NSInteger)collectionView:(UICollectionView *)collectionViewnumberOfItemsInSection:(NSInteger)section{

return[self.imagePathscount];

}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionViewcellForItemAtIndexPath:(NSIndexPath*)indexPath{

//dequeue cellUICollectionViewCell *cell = [collectionViewdequeueReusableCellWithReuseIdentifier:@"Cell"forIndexPath:indexPath];

//add image viewconstNSInteger imageTag =99;??

UIImageView *imageView = (UIImageView *)[cellviewWithTag:imageTag];

if(!imageView) {? ? ??

imageView = [[UIImageViewalloc]initWithFrame:cell.contentView.bounds];??

? ? imageView.tag= imageTag;? ?

? ? [cell.contentViewaddSubview:imageView];?

? }

//set imageNSString*imagePath = self.imagePaths[indexPath.row];

? imageView.image= [UIImageimageWithContentsOfFile:imagePath];

returncell;

}

@end

傳送器中的圖片尺寸為800x600像素的PNG,對(duì)iPhone5來說,1/60秒要加載大概700KB左右的圖片。當(dāng)傳送器滾動(dòng)的時(shí)候,圖片也在實(shí)時(shí)加載,于是(預(yù)期中的)卡動(dòng)就發(fā)生了。時(shí)間分析工具(圖14.2)顯示了很多時(shí)間都消耗在了UIImage的+imageWithContentsOfFile:方法中了。很明顯,圖片加載造成了瓶頸。

這里提升性能唯一的方式就是在另一個(gè)線程中加載圖片。這并不能夠降低實(shí)際的加載時(shí)間(可能情況會(huì)更糟,因?yàn)橄到y(tǒng)可能要消耗CPU時(shí)間來處理加載的圖片數(shù)據(jù)),但是主線程能夠有時(shí)間做一些別的事情,比如響應(yīng)用戶輸入,以及滑動(dòng)動(dòng)畫。

為了在后臺(tái)線程加載圖片,我們可以使用GCD或者NSOperationQueue創(chuàng)建自定義線程,或者使用CATiledLayer。為了從遠(yuǎn)程網(wǎng)絡(luò)加載圖片,我們可以使用異步的NSURLConnection,但是對(duì)本地存儲(chǔ)的圖片,并不十分有效。

GCD和NSOperationQueue

GCD(Grand Central Dispatch)和NSOperationQueue很類似,都給我們提供了隊(duì)列閉包塊來在線程中按一定順序來執(zhí)行。NSOperationQueue有一個(gè)Objecive-C接口(而不是使用GCD的全局C函數(shù)),同樣在操作優(yōu)先級(jí)和依賴關(guān)系上提供了很好的粒度控制,但是需要更多地設(shè)置代碼。

清單14.2顯示了在低優(yōu)先級(jí)的后臺(tái)隊(duì)列而不是主線程使用GCD加載圖片的-collectionView:cellForItemAtIndexPath:方法,然后當(dāng)需要加載圖片到視圖的時(shí)候切換到主線程,因?yàn)樵诤笈_(tái)線程訪問視圖會(huì)有安全隱患。

由于視圖在UICollectionView會(huì)被循環(huán)利用,我們加載圖片的時(shí)候不能確定是否被不同的索引重新復(fù)用。為了避免圖片加載到錯(cuò)誤的視圖中,我們?cè)诩虞d前把單元格打上索引的標(biāo)簽,然后在設(shè)置圖片的時(shí)候檢測標(biāo)簽是否發(fā)生了改變。

清單14.2 使用GCD加載傳送圖片

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView? ? ? ? ? ? ? ? ? ? cellForItemAtIndexPath:(NSIndexPath*)indexPath{

//dequeue cellUICollectionViewCell *cell = [collectionViewdequeueReusableCellWithReuseIdentifier:@"Cell"forIndexPath:indexPath];//add image viewconstNSInteger imageTag =99;??

UIImageView *imageView = (UIImageView *)[cellviewWithTag:imageTag];

if(!imageView)

{? ? ? ? imageView = [[UIImageViewalloc]initWithFrame:cell.contentView.bounds];? ? ? ? imageView.tag= imageTag;? ? ?

? [cell.contentViewaddSubview: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 imageNSIntegerindex= indexPath.row;NSString*imagePath = self.imagePaths[index];? ??

? UIImage *image = [UIImageimageWithContentsOfFile: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; }? ? ??

});? ? });

returncell;

}

當(dāng)運(yùn)行更新后的版本,性能比之前不用線程的版本好多了,但仍然并不完美(圖14.3)。

我們可以看到+imageWithContentsOfFile:方法并不在CPU時(shí)間軌跡的最頂部,所以我們的確修復(fù)了延遲加載的問題。問題在于我們假設(shè)傳送器的性能瓶頸在于圖片文件的加載,但實(shí)際上并不是這樣。加載圖片數(shù)據(jù)到內(nèi)存中只是問題的第一部分。

延遲解壓

一旦圖片文件被加載就必須要進(jìn)行解碼,解碼過程是一個(gè)相當(dāng)復(fù)雜的任務(wù),需要消耗非常長的時(shí)間。解碼后的圖片將同樣使用相當(dāng)大的內(nèi)存。

用于加載的CPU時(shí)間相對(duì)于解碼來說根據(jù)圖片格式而不同。對(duì)于PNG圖片來說,加載會(huì)比JPEG更長,因?yàn)槲募赡芨?,但是解碼會(huì)相對(duì)較快,而且Xcode會(huì)把PNG圖片進(jìn)行解碼優(yōu)化之后引入工程。JPEG圖片更小,加載更快,但是解壓的步驟要消耗更長的時(shí)間,因?yàn)镴PEG解壓算法比基于zip的PNG算法更加復(fù)雜。

當(dāng)加載圖片的時(shí)候,iOS通常會(huì)延遲解壓圖片的時(shí)間,直到加載到內(nèi)存之后。這就會(huì)在準(zhǔn)備繪制圖片的時(shí)候影響性能,因?yàn)樾枰诶L制之前進(jìn)行解壓(通常是消耗時(shí)間的問題所在)。

最簡單的方法就是使用UIImage的+imageNamed:方法避免延時(shí)加載。不像+imageWithContentsOfFile:(和其他別的UIImage加載方法),這個(gè)方法會(huì)在加載圖片之后立刻進(jìn)行解壓(就和本章之前我們談到的好處一樣)。問題在于+imageNamed:只對(duì)從應(yīng)用資源束中的圖片有效,所以對(duì)用戶生成的圖片內(nèi)容或者是下載的圖片就沒法使用了。

另一種立刻加載圖片的方法就是把它設(shè)置成圖層內(nèi)容,或者是UIImageView的image屬性。不幸的是,這又需要在主線程執(zhí)行,所以不會(huì)對(duì)性能有所提升。

第三種方式就是繞過UIKit,像下面這樣使用ImageIO框架:

NSInteger index = indexPath.row;

NSURL*imageURL = [NSURLfileURLWithPath:self.imagePaths[index]];

NSDictionary*options = @{(__bridgeid)kCGImageSourceShouldCache: @YES};

CGImageSourceRef source = CGImageSourceCreateWithURL((__bridgeCFURLRef)imageURL,NULL);CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source,0,(__bridgeCFDictionaryRef)options);

UIImage *image = [UIImageimageWithCGImage:imageRef];

CGImageRelease(imageRef);CFRelease(source);

這樣就可以使用kCGImageSourceShouldCache來創(chuàng)建圖片,強(qiáng)制圖片立刻解壓,然后在圖片的生命周期保留解壓后的版本。

最后一種方式就是使用UIKit加載圖片,但是立刻會(huì)知道CGContext中去。圖片必須要在繪制之前解壓,所以就強(qiáng)制了解壓的及時(shí)性。這樣的好處在于繪制圖片可以再后臺(tái)線程(例如加載本身)執(zhí)行,而不會(huì)阻塞UI。

有兩種方式可以為強(qiáng)制解壓提前渲染圖片:

將圖片的一個(gè)像素繪制成一個(gè)像素大小的CGContext。這樣仍然會(huì)解壓整張圖片,但是繪制本身并沒有消耗任何時(shí)間。這樣的好處在于加載的圖片并不會(huì)在特定的設(shè)備上為繪制做優(yōu)化,所以可以在任何時(shí)間點(diǎn)繪制出來。同樣iOS也就可以丟棄解壓后的圖片來節(jié)省內(nèi)存了。

將整張圖片繪制到CGContext中,丟棄原始的圖片,并且用一個(gè)從上下文內(nèi)容中新的圖片來代替。這樣比繪制單一像素那樣需要更加復(fù)雜的計(jì)算,但是因此產(chǎn)生的圖片將會(huì)為繪制做優(yōu)化,而且由于原始?jí)嚎s圖片被拋棄了,iOS就不能夠隨時(shí)丟棄任何解壓后的圖片來節(jié)省內(nèi)存了。

需要注意的是蘋果特別推薦了不要使用這些詭計(jì)來繞過標(biāo)準(zhǔn)圖片解壓邏輯(所以也是他們選擇用默認(rèn)處理方式的原因),但是如果你使用很多大圖來構(gòu)建應(yīng)用,那如果想提升性能,就只能和系統(tǒng)博弈了。

如果不使用+imageNamed:,那么把整張圖片繪制到CGContext可能是最佳的方式了。盡管你可能認(rèn)為多余的繪制相較別的解壓技術(shù)而言性能不是很高,但是新創(chuàng)建的圖片(在特定的設(shè)備上做過優(yōu)化)可能比原始圖片繪制的更快。

同樣,如果想顯示圖片到比原始尺寸小的容器中,那么一次性在后臺(tái)線程重新繪制到正確的尺寸會(huì)比每次顯示的時(shí)候都做縮放會(huì)更有效(盡管在這個(gè)例子中我們加載的圖片呈現(xiàn)正確的尺寸,所以不需要多余的優(yōu)化)。

如果修改了-collectionView:cellForItemAtIndexPath:方法來重繪圖片(清單14.3),你會(huì)發(fā)現(xiàn)滑動(dòng)更加平滑。

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView? ? ? ? ? ? ? ? ? cellForItemAtIndexPath:(NSIndexPath*)indexPath{

//dequeue cellUICollectionViewCell *cell = [collectionViewdequeueReusableCellWithReuseIdentifier:@"Cell"forIndexPath:indexPath];? ? ...//switch to background thread

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW,0), ^{

//load imageNSIntegerindex= indexPath.row;NSString*imagePath = self.imagePaths[index];? ?

? ? UIImage *image = [UIImageimageWithContentsOfFile:imagePath];//redraw image using device contextUIGraphicsBeginImageContextWithOptions(imageView.bounds.size,YES,0);? ? ? ? [imagedrawInRect:imageView.bounds];? ? ?

? image =UIGraphicsGetImageFromCurrentImageContext();UIGraphicsEndImageContext();//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;? ? ? ? ? ?

}? ? ?

? });});returncell;}

CATiledLayer

如第6章“專用圖層”中的例子所示,CATiledLayer可以用來異步加載和顯示大型圖片,而不阻塞用戶輸入。但是我們同樣可以使用CATiledLayer在UICollectionView中為每個(gè)表格創(chuàng)建分離的CATiledLayer實(shí)例加載傳動(dòng)器圖片,每個(gè)表格僅使用一個(gè)圖層。

這樣使用CATiledLayer有幾個(gè)潛在的弊端:

CATiledLayer的隊(duì)列和緩存算法沒有暴露出來,所以我們只能祈禱它能匹配我們的需求

CATiledLayer需要我們每次重繪圖片到CGContext中,即使它已經(jīng)解壓縮,而且和我們單元格尺寸一樣(因此可以直接用作圖層內(nèi)容,而不需要重繪)。

我們來看看這些弊端有沒有造成不同:清單14.4顯示了使用CATiledLayer對(duì)圖片傳送器的重新實(shí)現(xiàn)。

清單14.4 使用CATiledLayer的圖片傳送器

#import"ViewController.h"

#import@interfaceViewController() @property(nonatomic,copy)NSArray*imagePaths;

@property(nonatomic,weak)IBOutletUICollectionView *collectionView;

@end

@implementationViewController

- (void)viewDidLoad{

//set up data

self.imagePaths= [[NSBundlemainBundle]pathsForResourcesOfType:@"jpg"inDirectory:@"Vacation Photos"];??

[self.collectionViewregisterClass:[UICollectionViewCellclass]forCellWithReuseIdentifier:@"Cell"];

}

- (NSInteger)collectionView:(UICollectionView *)collectionViewnumberOfItemsInSection:(NSInteger)section{return[self.imagePathscount];

}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionViewcellForItemAtIndexPath:(NSIndexPath*)indexPath{

//dequeue cellUICollectionViewCell *cell = [collectionViewdequeueReusableCellWithReuseIdentifier:@"Cell"forIndexPath:indexPath];//add the tiled layerCATiledLayer *tileLayer = [cell.contentView.layer.sublayerslastObject];

if(!tileLayer) {? ? ? ? tileLayer = [CATiledLayerlayer];? ? ?

? tileLayer.frame= cell.bounds;? ? ? ?

tileLayer.contentsScale= [UIScreenmainScreen].scale;? ? ? ? tileLayer.tileSize=CGSizeMake(cell.bounds.size.width* [UIScreenmainScreen].scale, cell.bounds.size.height* [UIScreenmainScreen].scale);? ?

? tileLayer.delegate= self;? ? ? ? [tileLayersetValue:@(indexPath.row)forKey:@"index"];? ? ? ? [cell.contentView.layeraddSublayer:tileLayer];??

}//tag the layer with the correct index and reload

tileLayer.contents=nil;??

[tileLayersetValue:@(indexPath.row)forKey:@"index"];? ? [tileLayersetNeedsDisplay];

returncell;}

- (void)drawLayer:(CATiledLayer *)layerinContext:(CGContextRef)ctx{

//get image index

NSIntegerindex= [[layervalueForKey:@"index"]integerValue];

//load tile imageNSString*imagePath = self.imagePaths[index];??

UIImage *tileImage = [UIImageimageWithContentsOfFile:imagePath];

//calculate image rectCGFloat aspectRatio = tileImage.size.height/ tileImage.size.width;??

CGRect imageRect = CGRectZero;?

? imageRect.size.width= layer.bounds.size.width;??

imageRect.size.height= layer.bounds.size.height* aspectRatio;? ? imageRect.origin.y= (layer.bounds.size.height- imageRect.size.height)/2;/

/draw tile

UIGraphicsPushContext(ctx);? ?

[tileImagedrawInRect:imageRect];

UIGraphicsPopContext();

}

@end

需要解釋幾點(diǎn):

CATiledLayer的tileSize屬性單位是像素,而不是點(diǎn),所以為了保證瓦片和表格尺寸一致,需要乘以屏幕比例因子。

在-drawLayer:inContext:方法中,我們需要知道圖層屬于哪一個(gè)indexPath以加載正確的圖片。這里我們利用了CALayer的KVC來存儲(chǔ)和檢索任意的值,將圖層和索引打標(biāo)簽。

結(jié)果CATiledLayer工作的很好,性能問題解決了,而且和用GCD實(shí)現(xiàn)的代碼量差不多。僅有一個(gè)問題在于圖片加載到屏幕上后有一個(gè)明顯的淡入(圖14.4)。

我們可以調(diào)整CATiledLayer的fadeDuration屬性來調(diào)整淡入的速度,或者直接將整個(gè)漸變移除,但是這并沒有根本性地去除問題:在圖片加載到準(zhǔn)備繪制的時(shí)候總會(huì)有一個(gè)延遲,這將會(huì)導(dǎo)致滑動(dòng)時(shí)候新圖片的跳入。這并不是CATiledLayer的問題,使用GCD的版本也有這個(gè)問題。

即使使用上述我們討論的所有加載圖片和緩存的技術(shù),有時(shí)候仍然會(huì)發(fā)現(xiàn)實(shí)時(shí)加載大圖還是有問題。就和13章中提到的那樣,iPad上一整個(gè)視網(wǎng)膜屏圖片分辨率達(dá)到了2048x1536,而且會(huì)消耗12MB的RAM(未壓縮)。第三代iPad的硬件并不能支持1/60秒的幀率加載,解壓和顯示這種圖片。即使用后臺(tái)線程加載來避免動(dòng)畫卡頓,仍然解決不了問題。

我們可以在加載的同時(shí)顯示一個(gè)占位圖片,但這并沒有根本解決問題,我們可以做到更好

分辨率交換

視網(wǎng)膜分辨率(根據(jù)蘋果市場定義)代表了人的肉眼在正常視角距離能夠分辨的最小像素尺寸。但是這只能應(yīng)用于靜態(tài)像素。當(dāng)觀察一個(gè)移動(dòng)圖片時(shí),你的眼睛就會(huì)對(duì)細(xì)節(jié)不敏感,于是一個(gè)低分辨率的圖片和視網(wǎng)膜質(zhì)量的圖片沒什么區(qū)別了。

如果需要快速加載和顯示移動(dòng)大圖,簡單的辦法就是欺騙人眼,在移動(dòng)傳送器的時(shí)候顯示一個(gè)小圖(或者低分辨率),然后當(dāng)停止的時(shí)候再換成大圖。這意味著我們需要對(duì)每張圖片存儲(chǔ)兩份不同分辨率的副本,但是幸運(yùn)的是,由于需要同時(shí)支持Retina和非Retina設(shè)備,本來這就是普遍要做到的。

如果從遠(yuǎn)程源或者用戶的相冊(cè)加載沒有可用的低分辨率版本圖片,那就可以動(dòng)態(tài)將大圖繪制到較小的CGContext,然后存儲(chǔ)到某處以備復(fù)用。

為了做到圖片交換,我們需要利用UIScrollView的一些實(shí)現(xiàn)UIScrollViewDelegate協(xié)議的委托方法(和其他類似于UITableView和UICollectionView基于滾動(dòng)視圖的控件一樣):

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;

你可以使用這幾個(gè)方法來檢測傳送器是否停止?jié)L動(dòng),然后加載高分辨率的圖片。只要高分辨率圖片和低分辨率圖片尺寸顏色保持一致,你會(huì)很難察覺到替換的過程(確保在同一臺(tái)機(jī)器使用相同的圖像程序或者腳本生成這些圖片)。

緩存

如果有很多張圖片要顯示,最好不要提前把所有都加載進(jìn)來,而是應(yīng)該當(dāng)移出屏幕之后立刻銷毀。通過選擇性的緩存,你就可以避免來回滾動(dòng)時(shí)圖片重復(fù)性的加載了。

緩存其實(shí)很簡單:就是存儲(chǔ)昂貴計(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)存中自動(dòng)緩存了解壓后的圖片,即使你自己沒有保留對(duì)它的任何引用。

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

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

[UIImage imageNamed:]方法僅僅適用于在應(yīng)用程序資源束目錄下的圖片,但是大多數(shù)應(yīng)用的許多圖片都要從網(wǎng)絡(luò)或者是用戶的相機(jī)中獲取,所以[UIImage

imageNamed:]就沒法用了。

[UIImage imageNamed:]緩存用來存儲(chǔ)應(yīng)用界面的圖片(按鈕,背景等等)。如果對(duì)照片這種大圖也用這種緩存,那么iOS系統(tǒng)就很可能會(huì)移除這些圖片來節(jié)省內(nèi)存。那么在切換頁面時(shí)性能就會(huì)下降,因?yàn)檫@些圖片都需要重新加載。對(duì)傳送器的圖片使用一個(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í)時(shí)創(chuàng)建圖片,通常不太好生成一個(gè)字符串來區(qū)分別的圖片。在我們的圖片傳送帶例子中就很簡單,我們可以用圖片的文件名或者表格索引。

提前緩存 - 如果生成和加載數(shù)據(jù)的代價(jià)很大,你可能想當(dāng)?shù)谝淮涡枰玫降臅r(shí)候再去加載和緩存。提前加載的邏輯是應(yīng)用內(nèi)在就有的,但是在我們的例子中,這也非常好實(shí)現(xiàn),因?yàn)閷?duì)于一個(gè)給定的位置和滾動(dòng)方向,我們就可以精確地判斷出哪一張圖片將會(huì)出現(xiàn)。

緩存失效 -

如果圖片文件發(fā)生了變化,怎樣才能通知到緩存更新呢?這是個(gè)非常困難的問題(就像菲爾

卡爾頓提到的),但是幸運(yùn)的是當(dāng)從程序資源加載靜態(tài)圖片的時(shí)候并不需要考慮這些。對(duì)用戶提供的圖片來說(可能會(huì)被修改或者覆蓋),一個(gè)比較好的方式就是當(dāng)圖片緩存的時(shí)候打上一個(gè)時(shí)間戳以便當(dāng)文件更新的時(shí)候作比較。

緩存回收 - 當(dāng)內(nèi)存不夠的時(shí)候,如何判斷哪些緩存需要清空呢?這就需要到你寫一個(gè)合適的算法了。幸運(yùn)的是,對(duì)緩存回收的問題,蘋果提供了一個(gè)叫做NSCache通用的解決方案

NSCache

NSCache和NSDictionary類似。你可以通過-setObject:forKey:和-object:forKey:方法分別來插入,檢索。和字典不同的是,NSCache在系統(tǒng)低內(nèi)存的時(shí)候自動(dòng)丟棄存儲(chǔ)的對(duì)象。

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

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

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

使用圖片緩存和提前加載的實(shí)現(xiàn)來擴(kuò)展之前的傳送器案例,然后來看看是否效果更好(見清單14.5)。

清單14.5 添加緩存

#import"ViewController.h"

@interfaceViewController() @property(nonatomic,copy)NSArray*imagePaths;

@property(nonatomic,weak)IBOutletUICollectionView *collectionView;

@end

@implementationViewController- (void)viewDidLoad{

//set up data

self.imagePaths= [[NSBundlemainBundle]pathsForResourcesOfType:@"png"inDirectory:@"Vacation Photos"];//register cell class

[self.collectionViewregisterClass:[UICollectionViewCellclass]forCellWithReuseIdentifier:@"Cell"];

}

- (NSInteger)collectionView:(UICollectionView *)collectionViewnumberOfItemsInSection:(NSInteger)section{

??? return[self.imagePathscount];

}

- (UIImage *)loadImageAtIndex:(NSUInteger)index{

//set up cachestaticNSCache*cache =nil;if(!cache) {? ? ?

? cache = [[NSCachealloc]init];? ? }

//if already cached, return immediatelyUIImage *image = [cacheobjectForKey:@(index)];

if(image) {return[imageisKindOfClass:[NSNullclass]]?nil: image;? ? }

//set placeholder to avoid reloading image multiple times

[cachesetObject:[NSNullnull]forKey:@(index)];//switch to background thread

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW,0), ^{

//load imageNSString*imagePath = self.imagePaths[index];? ?

? ? UIImage *image = [UIImageimageWithContentsOfFile:imagePath];//redraw image using device contextUIGraphicsBeginImageContextWithOptions(image.size,YES,0);? ? ? ? [imagedrawAtPoint:CGPointZero];? ?

? ? image =UIGraphicsGetImageFromCurrentImageContext();UIGraphicsEndImageContext();/

/set image for correct image view

dispatch_async(dispatch_get_main_queue(), ^{

//cache the image[cachesetObject:imageforKey:@(index)];

//display the imageNSIndexPath*indexPath = [NSIndexPathindexPathForItem:indexinSection:0];

UICollectionViewCell *cell = [self.collectionViewcellForItemAtIndexPath:indexPath];? ? ? ? ? ? UIImageView *imageView = [cell.contentView.subviewslastObject];? ? ? ? ? ? imageView.image= image;? ? ??

});? ? });//not loaded yetreturnnil;}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionViewcellForItemAtIndexPath:(NSIndexPath*)indexPath{

//dequeue cellUICollectionViewCell *cell = [collectionViewdequeueReusableCellWithReuseIdentifier:@"Cell"forIndexPath:indexPath];//add image view

UIImageView *imageView = [cell.contentView.subviewslastObject];

if(!imageView) {? ??

? imageView = [[UIImageViewalloc]initWithFrame:cell.contentView.bounds];? ? ? ? imageView.contentMode= UIViewContentModeScaleAspectFit;? ? ? ? [cell.contentViewaddSubview:imageView];

? }

//set or load image for this index

imageView.image= [selfloadImageAtIndex:indexPath.item];

//preload image for previous and next index

if(indexPath.item< [self.imagePathscount] -1) {? ? ? ? [selfloadImageAtIndex:indexPath.item +1];

}if(indexPath.item>0) {? ? ??

[selfloadImageAtIndex:indexPath.item -1]; }returncell;}

@end

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

文件格式

圖片加載性能取決于加載大圖的時(shí)間和解壓小圖時(shí)間的權(quán)衡。很多蘋果的文檔都說PNG是iOS所有圖片加載的最好格式。但這是極度誤導(dǎo)的過時(shí)信息了。

PNG圖片使用的無損壓縮算法可以比使用JPEG的圖片做到更快地解壓,但是由于閃存訪問的原因,這些加載的時(shí)間并沒有什么區(qū)別。

清單14.6展示了標(biāo)準(zhǔn)的應(yīng)用程序加載不同尺寸圖片所需要時(shí)間的一些代碼。為了保證實(shí)驗(yàn)的準(zhǔn)確性,我們會(huì)測量每張圖片的加載和繪制時(shí)間來確??紤]到解壓性能的因素。另外每隔一秒重復(fù)加載和繪制圖片,這樣就可以取到平均時(shí)間,使得結(jié)果更加準(zhǔn)確。

#import"ViewController.h"

staticNSString*constImageFolder =@"Coast Photos";

@interfaceViewController() @property(nonatomic,copy)NSArray*items;

@property(nonatomic,weak)IBOutletUITableView *tableView;

@end

@implementationViewController

- (void)viewDidLoad{??

[superviewDidLoad];

//set up image namesself.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;

CFTimeIntervalendTime =0;

CFTimeIntervalstartTime =CFAbsoluteTimeGetCurrent();

while(endTime - startTime <1)

{//load image

UIImage *image = [UIImageimageWithContentsOfFile: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), ^{

//setupNSString*fileName = self.items[index];

NSString*pngPath = [[NSBundlemainBundle]pathForResource:filenameofType:@"png"inDirectory:ImageFolder];NSString*jpgPath = [[NSBundlemainBundle]pathForResource:filenameofType:@"jpg"inDirectory:ImageFolder];

//loadNSInteger pngTime = [selfloadImageForOneSec:pngPath] *1000;? ?

? ? NSInteger jpgTime = [selfloadImageForOneSec:jpgPath] *1000;

//updated UI on main thread

dispatch_async(dispatch_get_main_queue(), ^{//find table cell and updateNSIndexPath*indexPath = [NSIndexPathindexPathForRow:indexinSection:0];? ? ? ? ? ? UITableViewCell *cell = [self.tableViewcellForRowAtIndexPath:indexPath];? ? ? ? ? ? cell.detailTextLabel.text= [NSStringstringWithFormat:@"PNG:%03ims JPG:%03ims", pngTime, jpgTime];? ? ? ? });? ? });}

- (NSInteger)tableView:(UITableView *)tableViewnumberOfRowsInSection:(NSInteger)section{

????? return[self.itemscount];

}

- (UITableViewCell *)tableView:(UITableView *)tableViewcellForRowAtIndexPath:(NSIndexPath*)indexPath{/

/dequeue cell

UITableViewCell *cell = [self.tableViewdequeueReusableCellWithIdentifier:@"Cell"];if(!cell) {? ? ? ? cell = [[UITableViewCellalloc]initWithStyle:UITableViewCellStyleValue1reuseIdentifier:@"Cell"];? ? }

//set up cell

NSString*imageName = self.items[indexPath.row];?

? cell.textLabel.text= imageName;

? cell.detailTextLabel.text=@"Loading...";//load image[selfloadImageAtIndex:indexPath.row];returncell;}

@end

PNG和JPEG壓縮算法作用于兩種不同的圖片類型:JPEG對(duì)于噪點(diǎn)大的圖片效果很好;但是PNG更適合于扁平顏色,鋒利的線條或者一些漸變色的圖片。為了讓測評(píng)的基準(zhǔn)更加公平,我們用一些不同的圖片來做實(shí)驗(yàn):一張照片和一張彩虹色的漸變。JPEG版本的圖片都用默認(rèn)的Photoshop60%“高質(zhì)量”設(shè)置編碼。結(jié)果見圖片14.5。

如結(jié)果所示,相對(duì)于不友好的PNG圖片,相同像素的JPEG圖片總是比PNG加載更快,除非一些非常小的圖片、但對(duì)于友好的PNG圖片,一些中大尺寸的圖效果還是很好的。

所以對(duì)于之前的圖片傳送器程序來說,JPEG會(huì)是個(gè)不錯(cuò)的選擇。如果用JPEG的話,一些多線程和緩存策略都沒必要了。

但JPEG圖片并不是所有情況都適用。如果圖片需要一些透明效果,或者壓縮之后細(xì)節(jié)損耗很多,那就該考慮用別的格式了。蘋果在iOS系統(tǒng)中對(duì)PNG和JPEG都做了一些優(yōu)化,所以普通情況下都應(yīng)該用這種格式。也就是說在一些特殊的情況下才應(yīng)該使用別的格式。

混合圖片

對(duì)于包含透明的圖片來說,最好是使用壓縮透明通道的PNG圖片和壓縮RGB部分的JPEG圖片混合起來加載。這就對(duì)任何格式都適用了,而且無論從質(zhì)量還是文件尺寸還是加載性能來說都和PNG和JPEG的圖片相近。相關(guān)分別加載顏色和遮罩圖片并在運(yùn)行時(shí)合成的代碼見14.7。

#import"ViewController.h"

@interfaceViewController()

@property(nonatomic,weak)IBOutletUIImageView *imageView;

@end

@implementationViewController

- (void)viewDidLoad{? ? [superviewDidLoad];//load color image

UIImage *image = [UIImageimageNamed:@"Snowman.jpg"];

//load mask imageUIImage *mask = [UIImageimageNamed:@"SnowmanMask.png"];

//convert mask to correct format

CGColorSpaceRef graySpace =CGColorSpaceCreateDeviceGray();??

CGImageRef maskRef =CGImageCreateCopyWithColorSpace(mask.CGImage, graySpace);

CGColorSpaceRelease(graySpace);

//combine images

CGImageRef resultRef =CGImageCreateWithMask(image.CGImage, maskRef);??

UIImage *result = [UIImageimageWithCGImage:resultRef];

CGImageRelease(resultRef);

CGImageRelease(maskRef);//display result

self.imageView.image= result;}

@end

對(duì)每張圖片都使用兩個(gè)獨(dú)立的文件確實(shí)有些累贅。JPNG的庫(https://github.com/nicklockwood/JPNG)對(duì)這個(gè)技術(shù)提供了一個(gè)開源的可以復(fù)用的實(shí)現(xiàn),并且添加了直接使用+imageNamed:和+imageWithContentsOfFile:方法的支持。

JPEG2000

除了JPEG和PNG之外iOS還支持別的一些格式,例如TIFF和GIF,但是由于他們質(zhì)量壓縮得更厲害,性能比JPEG和PNG糟糕的多,所以大多數(shù)情況并不用考慮。

但是iOS之后,蘋果低調(diào)添加了對(duì)JPEG 2000圖片格式的支持,所以大多數(shù)人并不知道。它甚至并不被Xcode很好的支持 - JPEG 2000圖片都沒在Interface Builder中顯示。

但是JPEG 2000圖片在(設(shè)備和模擬器)運(yùn)行時(shí)會(huì)有效,而且比JPEG質(zhì)量更好,同樣也對(duì)透明通道有很好的支持。但是JPEG 2000圖片在加載和顯示圖片方面明顯要比PNG和JPEG慢得多,所以對(duì)圖片大小比運(yùn)行效率更敏感的時(shí)候,使用它是一個(gè)不錯(cuò)的選擇。

但仍然要對(duì)JPEG 2000保持關(guān)注,因?yàn)樵诤罄m(xù)iOS版本說不定就對(duì)它的性能做提升,但是在現(xiàn)階段,混合圖片對(duì)更小尺寸和質(zhì)量的文件性能會(huì)更好

PVRTC

當(dāng)前市場的每個(gè)iOS設(shè)備都使用了Imagination Technologies PowerVR圖像芯片作為GPU。PowerVR芯片支持一種叫做PVRTC(PowerVR Texture Compression)的標(biāo)準(zhǔn)圖片壓縮。

和iOS上可用的大多數(shù)圖片格式不同,PVRTC不用提前解壓就可以被直接繪制到屏幕上。這意味著在加載圖片之后不需要有解壓操作,所以內(nèi)存中的圖片比其他圖片格式大大減少了(這取決于壓縮設(shè)置,大概只有1/60那么大)。

但是PVRTC仍然有一些弊端:

盡管加載的時(shí)候消耗了更少的RAM,PVRTC文件比JPEG要大,有時(shí)候甚至比PNG還要大(這取決于具體內(nèi)容),因?yàn)閴嚎s算法是針對(duì)于性能,而不是文件尺寸。

PVRTC必須要是二維正方形,如果源圖片不滿足這些要求,那必須要在轉(zhuǎn)換成PVRTC的時(shí)候強(qiáng)制拉伸或者填充空白空間。

質(zhì)量并不是很好,尤其是透明圖片。通??雌饋砀駠?yán)重壓縮的JPEG文件。

PVRTC不能用Core Graphics繪制,也不能在普通的UIImageView顯示,也不能直接用作圖層的內(nèi)容。你必須要用作OpenGL紋理加載PVRTC圖片,然后映射到一對(duì)三角板來在CAEAGLLayer或者GLKView中顯示。

創(chuàng)建一個(gè)OpenGL紋理來繪制PVRTC圖片的開銷相當(dāng)昂貴。除非你想把所有圖片繪制到一個(gè)相同的上下文,不然這完全不能發(fā)揮PVRTC的優(yōu)勢(shì)。

PVRTC使用了一個(gè)不對(duì)稱的壓縮算法。盡管它幾乎立即解壓,但是壓縮過程相當(dāng)漫長。在一個(gè)現(xiàn)代快速的桌面Mac電腦上,它甚至要消耗一分鐘甚至更多來生成一個(gè)PVRTC大圖。因此在iOS設(shè)備上最好不要實(shí)時(shí)生成。

如果你愿意使用OpehGL,而且即使提前生成圖片也能忍受得了,那么PVRTC將會(huì)提供相對(duì)于別的可用格式來說非常高效的加載性能。比如,可以在主線程1/60秒之內(nèi)加載并顯示一張2048×2048的PVRTC圖片(這已經(jīng)足夠大來填充一個(gè)視網(wǎng)膜屏幕的iPad了),這就避免了很多使用線程或者緩存等等復(fù)雜的技術(shù)難度。

Xcode包含了一些命令行工具例如texturetool來生成PVRTC圖片,但是用起來很不方便(它存在于Xcode應(yīng)用程序束中),而且很受限制。一個(gè)更好的方案就是使用Imagination TechnologiesPVRTexTool,可以從http://www.imgtec.com/powervr/insider/sdkdownloads免費(fèi)獲得。

安裝了PVRTexTool之后,就可以使用如下命令在終端中把一個(gè)合適大小的PNG圖片轉(zhuǎn)換成PVRTC文件:

如你所見,非常不容易,如果你對(duì)在常規(guī)應(yīng)用中使用PVRTC圖片很感興趣的話(例如基于OpenGL的游戲),可以參考一下GLView的庫(https://github.com/nicklockwood/GLView),它提供了一個(gè)簡單的GLImageView類,重新實(shí)現(xiàn)了UIImageView的各種功能,但同時(shí)提供了PVRTC圖片,而不需要你寫任何OpenGL代碼。

總結(jié)

在這章中,我們研究了和圖片加載解壓相關(guān)的性能問題,并延展了一系列解決方案

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容