iOS --- 使用NSOperation和NSURLSession封裝一個(gè)串行下載器

本文介紹了使用NSOperation和NSURLSession來(lái)實(shí)現(xiàn)串行下載的需求.

為何要這樣

iOS中使用NSURLSession的NSURLSessionDownloadTask進(jìn)行下載:
對(duì)于NSURLSessionDownloadTask對(duì)象, 執(zhí)行resume方法之后, 即開(kāi)始下載任務(wù).
而下載進(jìn)度是通過(guò)NSURLSessionDelegate的對(duì)應(yīng)方法進(jìn)行更新.
這意味著在發(fā)起下載任務(wù)后, 實(shí)際的下載操作是異步執(zhí)行的.
如果順序發(fā)起多個(gè)下載任務(wù)(執(zhí)行resume方法), 各個(gè)任務(wù)的下載情況完全是在NSURLSessionDelegate的回調(diào)方法中體現(xiàn). 這樣會(huì)出現(xiàn)幾個(gè)問(wèn)題:

  • 多任務(wù)同時(shí)下載: 在iOS上NSURLSession允許4個(gè)任務(wù)同時(shí)下載该贾,在一些應(yīng)用體驗(yàn)上其實(shí)不如單個(gè)順序下載(如音樂(lè)下載, 相機(jī)AR素材包下載等, 與其多首歌曲同時(shí)下載, 不如優(yōu)先下載完一首, 用戶可以盡快使用).
  • 任務(wù)間有依賴關(guān)系: 如AR素材包本身下載完成之后, 還要依賴另外的一個(gè)配置文件(Config.zip)等下載完成, 則即使該AR素材包下載完成, 但依然無(wú)法使用, 不能置為已下載狀態(tài).
  • 優(yōu)先級(jí)問(wèn)題: 如有的任務(wù)的優(yōu)先級(jí)比較高, 則需要做到優(yōu)先下載.
  • 下載完成時(shí)間不確定: 如上的使用場(chǎng)景, 因AR素材包和依賴文件的下載完成順序也不確定, 導(dǎo)致必須采用一些機(jī)制去觸發(fā)全部下載完畢的后續(xù)操作(如通知等).
  • 下載超時(shí): NSURLSessionDownloadTask對(duì)象執(zhí)行resume后, 如果在指定時(shí)間內(nèi)未能下載完畢會(huì)出現(xiàn)下載超時(shí), 多個(gè)任務(wù)同時(shí)下載時(shí)容易出現(xiàn).

目標(biāo)

以上邊講的AR素材包的場(chǎng)景為例, 我們想要實(shí)現(xiàn)一個(gè)下載機(jī)制:

  • 順序點(diǎn)擊多個(gè)AR素材, 發(fā)起多個(gè)下載請(qǐng)求, 但優(yōu)先下載一個(gè)素材包, 以便用戶可以盡快體驗(yàn)效果.
  • 對(duì)于有依賴關(guān)系的素材包, 先下載其依賴的配置文件, 再下載素材包本身, 素材包本身的下載完成狀態(tài)即是該AR整體的下載完成狀態(tài).

實(shí)現(xiàn)過(guò)程

綜合以上的需求, 使用NSOperation來(lái)封裝下載任務(wù), 但需要監(jiān)控其狀態(tài). 使用NSOperationQueue來(lái)管理這些下載任務(wù).

NSOperation的使用

CSDownloadOperation繼承自NSOperation, 不過(guò)對(duì)于其executing, finished, cancelled狀態(tài), 需要使用KVO監(jiān)控.

因?yàn)镵VO依賴于屬性的setter方法, 而NSOperation的這三個(gè)屬性是readonly的, 所以NSOperation在執(zhí)行中的這些狀態(tài)變化不會(huì)自動(dòng)觸發(fā)KVO, 而是需要我們額外做一些工作來(lái)手動(dòng)觸發(fā)KVO.

其實(shí), 可以簡(jiǎn)單理解為給NSOperation的這三個(gè)屬性自定義setter方法, 以便在其狀態(tài)變化時(shí)觸發(fā)KVO.

@interface CSDownloadOperation : NSOperation

@end

@interface CSDownloadOperation ()

// 因這些屬性是readonly, 不會(huì)自動(dòng)觸發(fā)KVO. 需要手動(dòng)觸發(fā)KVO, 見(jiàn)setter方法.
@property (assign, nonatomic, getter = isExecuting)     BOOL executing;
@property (assign, nonatomic, getter = isFinished)      BOOL finished;
@property (assign, nonatomic, getter = isCancelled)     BOOL cancelled;

@end

@implementation CSDownloadOperation

@synthesize executing       = _executing;
@synthesize finished        = _finished;
@synthesize cancelled       = _cancelled;


- (void)setExecuting:(BOOL)executing
{
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    [self didChangeValueForKey:@"isExecuting"];
}

- (void)setFinished:(BOOL)finished
{
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    [self didChangeValueForKey:@"isFinished"];
}

- (void)setCancelled:(BOOL)cancelled
{
    [self willChangeValueForKey:@"isCancelled"];
    _cancelled = cancelled;
    [self didChangeValueForKey:@"isCancelled"];
}

@end

NSOperation執(zhí)行時(shí), 發(fā)起NSURLSessionDownloadTask的下載任務(wù)(執(zhí)行resume方法), 然后等待該任務(wù)下載完成, 才去更新NSOperation的下載完成狀態(tài). 然后NSOperationQueue才能發(fā)起下一個(gè)任務(wù)的下載.

在初始化方法中, 構(gòu)建好NSURLSessionDownloadTask對(duì)象, 及下載所需的一些配置等.

- (void)p_setupDownload {
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
    self.urlSession = [NSURLSession sessionWithConfiguration:config
                                                    delegate:self
                                               delegateQueue:[NSOperationQueue mainQueue]];

    NSURL *url = [NSURL URLWithString:self.downloadItem.urlString];
    NSURLRequest *request = [NSURLRequest requestWithURL:url
                                             cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
                                         timeoutInterval:kTimeoutIntervalDownloadOperation];
    self.downloadTask = [self.urlSession downloadTaskWithRequest:request];
    self.downloadTask.taskDescription = self.downloadItem.urlString;
}

重寫其start, main和cancel方法:

/**
 必須重寫start方法.
 若不重寫start, 則cancel掉一個(gè)op, 會(huì)導(dǎo)致queue一直卡住.
 */
- (void)start
{
//    NSLog(@"%s %@", __func__, self);

    // 必須設(shè)置finished為YES, 不然也會(huì)卡住
    if ([self p_checkCancelled]) {
        return;
    }

    self.executing  = YES;

    [self main];
}

- (void)main
{
    if ([self p_checkCancelled]) {
        return;
    }

    [self p_startDownload];

    while (self.executing) {
        if ([self p_checkCancelled]) {
            return;
        }
    }
}

- (void)cancel
{
    [super cancel];

    [self p_didCancel];
}

在p_startDownload方法中發(fā)起下載:

- (void)p_startDownload
{
    [self.downloadTask resume];
}

使用NSURLSessionDownloadDelegate來(lái)更新下載狀態(tài)

實(shí)現(xiàn)該協(xié)議的回調(diào)方法, 更新下載進(jìn)度, 下載完成時(shí)更新?tīng)顟B(tài).

- (void)URLSession:(NSURLSession *)session
      downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
    // xxx
    [self p_done];
    // xxx
}

/* Sent periodically to notify the delegate of download progress. */
- (void)URLSession:(NSURLSession *)session
      downloadTask:(NSURLSessionDownloadTask *)downloadTask
      didWriteData:(int64_t)bytesWritten
 totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
    CGFloat progress = 1.0 * totalBytesWritten / totalBytesExpectedToWrite;
    // xxx
    // 更新下載進(jìn)度等
    // xxx
}

- (void)p_done
{
//    NSLog(@"%s %@", __func__, self);

    [self.urlSession finishTasksAndInvalidate];
    self.urlSession = nil;

    self.executing  = NO;
    self.finished   = YES;
}

使用NSOperationQueue來(lái)管理串行下載隊(duì)列

NSOperation中發(fā)起下載之后, 并不會(huì)立即設(shè)置其finished為YES, 而是會(huì)有一個(gè)while循環(huán), 一直等到NSURLSessionDownloadDelegate的回調(diào)方法執(zhí)行, 才會(huì)更新其finished狀態(tài).

而NSOperationQueue的特點(diǎn)就是上一個(gè)NSOperation的finished狀態(tài)未置為YES, 不會(huì)開(kāi)始下一個(gè)NSOperation的執(zhí)行.

設(shè)置優(yōu)先級(jí)

對(duì)NSOperation的優(yōu)先級(jí)進(jìn)行設(shè)置即可.

CSDownloadOperationQueue *queue = [CSDownloadOperationQueue sharedInstance];
CSDownloadOperation *op = [[CSDownloadOperation alloc] initWithDownloadItem:downloadItem
                                                               onOperationQueue:queue];
op.downloadDelegate = self;

// AR背景的優(yōu)先級(jí)提升
op.queuePriority = NSOperationQueuePriorityHigh;

獲取下載進(jìn)度及下載完成狀態(tài)

通過(guò)實(shí)現(xiàn)CSDownloadOperationQueueDelegate, 以觀察者的身份來(lái)接收下載進(jìn)度及下載完成狀態(tài).

// MARK: - CSDownloadOperationQueueDelegate

/**
 CSDownloadOperationQueueDelegate通知obsever來(lái)更新下載進(jìn)度
 */
@protocol CSDownloadOperationQueueDelegate <NSObject>

@optional
- (void)CSDownloadOperationQueue:(CSDownloadOperationQueue *)operationQueue
               downloadOperation:(CSDownloadOperation *)operation
             downloadingProgress:(CGFloat)progress;

- (void)CSDownloadOperationQueue:(CSDownloadOperationQueue *)operationQueue
               downloadOperation:(CSDownloadOperation *)operation
                downloadFinished:(BOOL)isSuccessful;

@end

注意這里觀察者模式的使用:
observer為繼承delegate的對(duì)象, 內(nèi)存管理語(yǔ)義當(dāng)然為weak.

// MARK: - observer

/**
 use observer to notify the downloading progress and result
 */
- (void)addObserver:(id<CSDownloadOperationQueueDelegate>)observer;
- (void)removeObserver:(id<CSDownloadOperationQueueDelegate>)observer;

所以, 需要使用NSValue的nonretainedObjectValue. 除此之外, 可以使用NSPointerArray來(lái)實(shí)現(xiàn)弱引用對(duì)象的容器.

- (NSMutableArray <NSValue *> *)observers {
    if (!_observers) {
        _observers = [NSMutableArray array];
    }

    return _observers;
}

- (void)addObserver:(id<CSDownloadOperationQueueDelegate>)observer {
    @synchronized (self.observers) {
        BOOL isExisting = NO;

        for (NSValue *value in self.observers) {
            if ([value.nonretainedObjectValue isEqual:observer]) {
                isExisting = YES;
                break;
            }
        }

        if (!isExisting) {
            [self.observers addObject:[NSValue valueWithNonretainedObject:observer]];
            NSLog(@"@");
        }
    }
}

- (void)removeObserver:(id<CSDownloadOperationQueueDelegate>)observer {
    @synchronized (self.observers) {
        NSValue *existingValue = nil;

        for (NSValue *value in self.observers) {
            if ([value.nonretainedObjectValue isEqual:observer]) {
                existingValue = value;
                break;
            }
        }

        if (existingValue) {
            [self.observers removeObject:existingValue];
        }
    }
}

Demo地址

CSSerialDownloader

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末岸裙,一起剝皮案震驚了整個(gè)濱河市重绷,隨后出現(xiàn)的幾起案子局义,更是在濱河造成了極大的恐慌,老刑警劉巖脚牍,帶你破解...
    沈念sama閱讀 216,544評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡逗柴,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門顿肺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)戏溺,“玉大人,你說(shuō)我怎么就攤上這事屠尊】趸觯” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 162,764評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵讼昆,是天一觀的道長(zhǎng)托享。 經(jīng)常有香客問(wèn)我,道長(zhǎng)控淡,這世上最難降的妖魔是什么嫌吠? 我笑而不...
    開(kāi)封第一講書人閱讀 58,193評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮掺炭,結(jié)果婚禮上辫诅,老公的妹妹穿的比我還像新娘。我一直安慰自己涧狮,他們只是感情好炕矮,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著者冤,像睡著了一般肤视。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上涉枫,一...
    開(kāi)封第一講書人閱讀 51,182評(píng)論 1 299
  • 那天邢滑,我揣著相機(jī)與錄音,去河邊找鬼愿汰。 笑死困后,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的衬廷。 我是一名探鬼主播摇予,決...
    沈念sama閱讀 40,063評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼吗跋!你這毒婦竟也來(lái)了侧戴?” 一聲冷哼從身側(cè)響起宁昭,我...
    開(kāi)封第一講書人閱讀 38,917評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎酗宋,沒(méi)想到半個(gè)月后积仗,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡本缠,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評(píng)論 2 332
  • 正文 我和宋清朗相戀三年斥扛,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片丹锹。...
    茶點(diǎn)故事閱讀 39,722評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖芬失,靈堂內(nèi)的尸體忽然破棺而出楣黍,到底是詐尸還是另有隱情布蔗,我是刑警寧澤轨香,帶...
    沈念sama閱讀 35,425評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站喊儡,受9級(jí)特大地震影響颊糜,放射性物質(zhì)發(fā)生泄漏哩治。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評(píng)論 3 326
  • 文/蒙蒙 一衬鱼、第九天 我趴在偏房一處隱蔽的房頂上張望业筏。 院中可真熱鬧,春花似錦鸟赫、人聲如沸蒜胖。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,671評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)台谢。三九已至,卻和暖如春岁经,著一層夾襖步出監(jiān)牢的瞬間朋沮,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,825評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工缀壤, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留樊拓,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,729評(píng)論 2 368
  • 正文 我出身青樓诉位,卻偏偏與公主長(zhǎng)得像骑脱,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子苍糠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容