14.1 加載和潛伏

2018-02-24 15:07 更新

加載和潛伏

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

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

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

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

????有時候圖片也需要從遠程網(wǎng)絡連接中下載,這將會比從磁盤加載要消耗更多的時間,甚至可能由于連接問題而加載失敗(在幾秒鐘嘗試之后)。你不能夠在主線程中加載網(wǎng)絡造成等待,所以需要后臺線程。

線程加載

????在第12章“性能調(diào)優(yōu)”我們的聯(lián)系人列表例子中,圖片都非常小,所以可以在主線程同步加載。但是對于大圖來說,這樣做就不太合適了,因為加載會消耗很長時間,造成滑動的不流暢?;瑒觿赢嫊谥骶€程的run loop中更新,所以會有更多運行在渲染服務進程中CPU相關(guān)的性能問題。

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

清單14.1 使用UICollectionView實現(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 時間分析工具展示了CPU瓶頸

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

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

GCD和NSOperationQueue

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

????清單14.2顯示了在低優(yōu)先級的后臺隊列而不是主線程使用GCD加載圖片的-collectionView:cellForItemAtIndexPath:方法,然后當需要加載圖片到視圖的時候切換到主線程,因為在后臺線程訪問視圖會有安全隱患。

????由于視圖在UICollectionView會被循環(huán)利用,我們加載圖片的時候不能確定是否被不同的索引重新復用。為了避免圖片加載到錯誤的視圖中,我們在加載前把單元格打上索引的標簽,然后在設置圖片的時候檢測標簽是否發(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;
}

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

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

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號