
??背景:使用CollectionView加載11張圖片,每張圖片大小是800*600,一屏展示。
??分析:在iPhone 5c上,進(jìn)頁(yè)面明顯有1s以上的延遲;在iPhone 8上,能感覺卡一下再進(jìn)入。一張圖片710KB,11張圖片是7.81MB,性能強(qiáng)悍如iPhone8依然感覺到卡。
??一直保持著測(cè)卡頓就要兼容性能最差的機(jī)器,我使用iPhone5c。
??先貼一份原始代碼:
@interface ImageIOViewController ()
@property (weak, nonatomic) IBOutlet UICollectionView *collectionView;
@property (nonatomic, copy) NSArray *imagePaths;
@end
@implementation ImageIOViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
self.imagePaths = [[NSBundle mainBundle] pathsForResourcesOfType:@"png" inDirectory:@"Vacation Photos"];
//register cell class
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"Cell"];
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return CGSizeMake(CGRectGetWidth(collectionView.frame)/7, CGRectGetWidth(collectionView.frame)/7*4.0/3);
}
- (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];
}
cell.tag = indexPath.row;
imageView.image = nil;
NSString *imagePath = self.imagePaths[indexPath.row];
imageView.image = [UIImage imageWithContentsOfFile:imagePath];
return cell;
}
@end
方法1:后臺(tái)線程加載
- (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];
}
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;
}
??這里我們需要處理一下顯示的圖片和想展示的位置要對(duì)應(yīng),要不然會(huì)亂掉。發(fā)現(xiàn)效果還不錯(cuò),卡頓沒有了,進(jìn)頁(yè)面的時(shí)候圖片無序的加載。這也是大家都能想到的思路。但有沒有其他方案呢?
方法2:延遲解壓
??一旦圖片文件被加載就必須要進(jìn)行解碼,解碼過程是一個(gè)相當(dāng)復(fù)雜的任務(wù),需要消耗非常長(zhǎng)的時(shí)間。解碼后的圖片將同樣使用相當(dāng)大的內(nèi)存。
??有三種方法來實(shí)現(xiàn)延遲解壓:
??1.最簡(jiǎn)單的方法就是使用 UIImage 的 +imageNamed: 方法避免延時(shí)加載。問題在于 +imageNamed: 只對(duì)從應(yīng)用資源束中的圖片有效,所以對(duì)用戶生成的圖片內(nèi)容或者是下載的圖片就沒法使用了。
??2.另一種立刻加載圖片的方法就是把它設(shè)置成圖層內(nèi)容,或者是 UIImageView 的 image 屬性。不幸的是,這又需要在主線程執(zhí)行,所以不會(huì)對(duì)性能有所提升。
??3.第三種方式就是繞過 UIKit ,像下面這樣使用ImageIO框架:
NSInteger index = indexPath.row;
NSURL *imageURL = [NSURL fileURLWithPath:self.imagePaths[index]];
NSDictionary *options = @{(__bridge id)kCGImageSourceShouldCache: @YES};
CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)imageURL, NULL);
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, 0,(__bridge CFDictionaryRef)options);
UIImage *image = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);
CFRelease(source);
這樣就可以使用 kCGImageSourceShouldCache 來創(chuàng)建圖片,強(qiáng)制圖片立刻解壓,然后在圖片的生命周期保留解壓后的版本。
最后一種方式就是使用UIKit加載圖片,但是立刻會(huì)知道 CGContext 中去。圖片必須要在繪制之前解壓,所以就強(qiáng)制了解壓的及時(shí)性。這樣的好處在于繪制圖片可以再后臺(tái)線程(例如加載本身)執(zhí)行,而不會(huì)阻塞UI。
- (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];
}
cell.tag = indexPath.row;
imageView.image = nil;
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];
//redraw image using device context
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, YES, 0);
[image drawInRect: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;
}
});
});
}
??注意不要在子線程訪問imageView的屬性,否則XCode會(huì)給提示,需要繪制的大小我們是可以提前知道的。
方法3 CATiledLayer替代UIImageView
??CATiledLayer 可以用來異步加載和顯示大型圖片,而不阻塞用戶輸入。
@interface ImageTiledLayerIOViewController ()<CALayerDelegate>
@property (nonatomic,strong) NSMutableSet<CATiledLayer *> * tiledLayerSet;
@property (weak, nonatomic) IBOutlet UICollectionView *collectionView;
@property (nonatomic, copy) NSArray *imagePaths;
@end
@implementation ImageTiledLayerIOViewController
- (void)viewDidLoad {
[super viewDidLoad];
_tiledLayerSet = [NSMutableSet new];
// Do any additional setup after loading the view from its nib.
self.imagePaths = [[NSBundle mainBundle] pathsForResourcesOfType:@"png" inDirectory:@"Vacation Photos"];
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"Cell"];
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return CGSizeMake(CGRectGetWidth(collectionView.frame), CGRectGetWidth(collectionView.frame)*4.0/3);
}
- (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 the tiled layer
CATiledLayer *tileLayer = [cell.contentView.layer.sublayers lastObject];
if (!tileLayer) {
tileLayer = [CATiledLayer layer];
tileLayer.frame = cell.bounds;
tileLayer.contentsScale = [UIScreen mainScreen].scale;
tileLayer.tileSize = CGSizeMake(cell.bounds.size.width * [UIScreen mainScreen].scale, cell.bounds.size.height * [UIScreen mainScreen].scale);
tileLayer.delegate = self;
[tileLayer setValue:@(indexPath.row) forKey:@"index"];
[cell.contentView.layer addSublayer:tileLayer];
[_tiledLayerSet addObject:tileLayer];
}
//tag the layer with the correct index and reload
tileLayer.contents = nil;
[tileLayer setValue:@(indexPath.row) forKey:@"index"];
[tileLayer setNeedsDisplay];
return cell;
}
- (void)drawLayer:(CATiledLayer *)layer inContext:(CGContextRef)ctx
{
//get image index
NSInteger index = [[layer valueForKey:@"index"] integerValue];
//load tile image
NSString *imagePath = self.imagePaths[index];
UIImage *tileImage = [UIImage imageWithContentsOfFile:imagePath];
//calculate image rect
CGFloat 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);
[tileImage drawInRect:imageRect];
UIGraphicsPopContext();
}
- (void)dealloc{
for (CATiledLayer * tiledLayer in _tiledLayerSet.allObjects) {
[tiledLayer removeFromSuperlayer];
}
}
??CATiledLayer需要在dealloc中手動(dòng)異常,否則會(huì)產(chǎn)生崩潰。其實(shí)后臺(tái)線程加載、延遲解壓或者使用CATiledLayer的方式已經(jīng)解決的很好了,那我們還有別的方式嗎?
方法4 緩存與后臺(tái)線程的結(jié)合
??如果是應(yīng)用程序資源下的圖片用 [UIImage imageNamed:] 足以解決我們的問題,但多數(shù)情況下是網(wǎng)絡(luò)圖片。我們可以自定義緩存,當(dāng)然蘋果也為我們提供了一種緩存方案NSCache.
??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: 方法來指定全體緩存的尺寸。
- (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;
}
方法5 分辨率交換
視網(wǎng)膜分辨率(根據(jù)蘋果市場(chǎng)定義)代表了人的肉眼在正常視角距離能夠分辨的最小像素尺寸。但是這只能應(yīng)用于靜態(tài)像素。當(dāng)觀察一個(gè)移動(dòng)圖片時(shí),你的眼睛就會(huì)對(duì)細(xì)節(jié)不敏感,于是一個(gè)低分辨率的圖片和視網(wǎng)膜質(zhì)量的圖片沒什么區(qū)別了。
??為了做到圖片交換,我們需要利用 UIScrollView 的一些實(shí)
現(xiàn) UIScrollViewDelegate 協(xié)議的委托方法(和其他類似于 UITableView 和 UICollectionView 基于滾動(dòng)視圖的控件一樣):
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
??你可以使用這幾個(gè)方法來檢測(cè)傳送器是否停止?jié)L動(dòng),然后加載高分辨率的圖片。只要高分辨率圖片和低分辨率圖片尺寸顏色保持一致,你會(huì)很難察覺到替換的過程(確保在同一臺(tái)機(jī)器使用相同的圖像程序或者腳本生成這些圖片)
小結(jié):上圖雖然是書中提出的方案,但沒有具體實(shí)現(xiàn)。實(shí)際情況采用大小圖,列表僅加載小圖,點(diǎn)擊放大再展示大圖。
方法6 使用RunLoop
??我們可以使用[self performSelector:@selector(loadImage) withObject:nil afterDelay:0 inModes:@[NSDefaultRunLoopMode]],僅在RunLoop休眠時(shí)加載圖片。可與其他幾種方式結(jié)合使用。
總結(jié):
1.蘋果一直以流暢著稱,多數(shù)情況下并不需要考慮性能問題,但涉及復(fù)雜場(chǎng)景,還是需要優(yōu)化的。
2.本書多數(shù)內(nèi)容參考<<iOS CoreAnimation>>,有興趣同學(xué)可自行閱讀。本文demo包含iOS CoreAnimation中所有案例。
3.第四種其實(shí)使用SDWebImage就能很好解決了,不需要我們?cè)賮韺?shí)現(xiàn)。第三種可以說是很強(qiáng)大,如果你圖片有1個(gè)G,你可以分成幾等分,用CATiledLayer加載,效果才是真的好。第五種的缺點(diǎn)需要提供大小圖了。
4.有任何問題歡迎留言評(píng)論。