本文介紹了使用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];
}
}
}