iOS性能優(yōu)化2:列表加載大圖優(yōu)化

效果圖

??背景:使用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)論。

最后編輯于
?著作權(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),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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