14.1 加載和潛伏

2018-02-24 15:07 更新

加載和潛伏

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

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

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

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

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

線程加載

????在第12章“性能調(diào)優(yōu)”我們的聯(lián)系人列表例子中,圖片都非常小,所以可以在主線程同步加載。但是對(duì)于大圖來(lái)說(shuō),這樣做就不太合適了,因?yàn)榧虞d會(huì)消耗很長(zhǎng)時(shí)間,造成滑動(dòng)的不流暢?;瑒?dòng)動(dòng)畫會(huì)在主線程的run loop中更新,所以會(huì)有更多運(yùn)行在渲染服務(wù)進(jìn)程中CPU相關(guān)的性能問(wèn)題。

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

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

#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];
}

- (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];
    }
    //set image
    NSString *imagePath = self.imagePaths[indexPath.row];
    imageView.image = [UIImage imageWithContentsOfFile:imagePath];
    return cell;
}

@end

圖14.2 時(shí)間分析工具展示了CPU瓶頸

????這里提升性能唯一的方式就是在另一個(gè)線程中加載圖片。這并不能夠降低實(shí)際的加載時(shí)間(可能情況會(huì)更糟,因?yàn)橄到y(tǒng)可能要消耗CPU時(shí)間來(lái)處理加載的圖片數(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ì)列閉包塊來(lái)在線程中按一定順序來(lái)執(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)線程訪問(wèn)視圖會(huì)有安全隱患。

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

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

- (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];
    }
    //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 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;
}

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

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

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)