系列文章:
在上一章節中我們通過一個具體的實例講解了AF是如何處理一下網絡請求的,本章將通過下載的實例(上傳的實現類似不再做額外分析)作為入口,再做進一步分析。
1.NSURLSessionDownloadTask
AFHTTPSessionManager *session = [AFHTTPSessionManager manager];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"https://dn-arnold.qbox.me/Snip.zip"]
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:10];
self.downloadTask = [session downloadTaskWithRequest:request
progress:^(NSProgress * _Nonnull downloadProgress) {
CGFloat progress = 1.0 * downloadProgress.completedUnitCount / downloadProgress.totalUnitCount;
NSLog(@"下載進度 :%.2f",progress);
}
destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
// 下載文件存儲的路徑
NSString * path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
path = [path stringByAppendingPathComponent:response.suggestedFilename];
NSLog(@"下載路徑 :%@",path);
return [NSURL fileURLWithPath:path];
} completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
if(!error){
NSLog(@"下載完成: %@",[filePath path]);
}
}];
[_downloadTask resume];
1.1 初始化task
// 1.根據請求獲取下載的task
url_session_manager_create_task_safely(^{
downloadTask = [self.session downloadTaskWithRequest:request];
});
AFURLSessionManagerTaskDelegate *delegate = [[AFURLSessionManagerTaskDelegate alloc] init];
delegate.manager = self;
delegate.completionHandler = completionHandler;
// 2.將傳入的下載路徑的block存儲到代理實例中
if (destination) {
delegate.downloadTaskDidFinishDownloading = ^NSURL * (NSURLSession * __unused session, NSURLSessionDownloadTask *task, NSURL *location) {
return destination(location, task.response);
};
}
downloadTask.taskDescription = self.taskDescriptionForSessionTasks;
[self setDelegate:delegate forTask:downloadTask];
// 3.將下載進度的block也存儲到代理對象中
delegate.downloadProgressBlock = downloadProgressBlock;
1.2 下載完成
AFURLSessionManager.m
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
AFURLSessionManagerTaskDelegate *delegate = [self delegateForTask:downloadTask];
if (self.downloadTaskDidFinishDownloading) {
NSURL *fileURL = self.downloadTaskDidFinishDownloading(session, downloadTask, location);
if (fileURL) {
delegate.downloadFileURL = fileURL;
NSError *error = nil;
[[NSFileManager defaultManager] moveItemAtURL:location toURL:fileURL error:&error];
if (error) {
[[NSNotificationCenter defaultCenter] postNotificationName:AFURLSessionDownloadTaskDidFailToMoveFileNotification object:downloadTask userInfo:error.userInfo];
}
return;
}
}
// task綁定的deledate也執行共有的數據處理方法
if (delegate) {
[delegate URLSession:session downloadTask:downloadTask didFinishDownloadingToURL:location];
}
}
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
NSError *fileManagerError = nil;
self.downloadFileURL = nil;
if (self.downloadTaskDidFinishDownloading) {
self.downloadFileURL = self.downloadTaskDidFinishDownloading(session, downloadTask, location);
if (self.downloadFileURL) {
[[NSFileManager defaultManager] moveItemAtURL:location toURL:self.downloadFileURL error:&fileManagerError];
if (fileManagerError) {
[[NSNotificationCenter defaultCenter] postNotificationName:AFURLSessionDownloadTaskDidFailToMoveFileNotification object:downloadTask userInfo:fileManagerError.userInfo];
}
}
}
}
上述的兩個實現,達到的目的是一樣的:獲取下載存儲的路徑,將下載的文件move到傳入的下載的路徑。但是為什么需要實現兩遍?原來是AFURLSessionManager提供的對外的接口,可以將下載路徑的回調傳入到session中。
接口:
/**
Sets a block to be executed when a download task has completed a download, as handled by the `NSURLSessionDownloadDelegate` method `URLSession:downloadTask:didFinishDownloadingToURL:`.
@param block A block object to be executed when a download task has completed. The block returns the URL the download should be moved to, and takes three arguments: the session, the download task, and the temporary location of the downloaded file. If the file manager encounters an error while attempting to move the temporary file to the destination, an `AFURLSessionDownloadTaskDidFailToMoveFileNotification` will be posted, with the download task as its object, and the user info of the error.
*/
- (void)setDownloadTaskDidFinishDownloadingBlock:(nullable NSURL * _Nullable (^)(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, NSURL *location))block;
使用:
[session setDownloadTaskDidFinishDownloadingBlock:^NSURL * _Nullable(NSURLSession * _Nonnull session, NSURLSessionDownloadTask * _Nonnull downloadTask, NSURL * _Nonnull location) {
NSString * path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
path = [path stringByAppendingPathComponent:@"result.zip"];
return [NSURL fileURLWithPath:path];
}];
1.3 下載進度
session可以調用如下的接口傳入下載進度與重啟下載的回調,以便實現自己的可定制的邏輯。
/**
Sets a block to be executed periodically to track download progress, as handled by the `NSURLSessionDownloadDelegate` method `URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesWritten:totalBytesExpectedToWrite:`.
@param block A block object to be called when an undetermined number of bytes have been downloaded from the server. This block has no return value and takes five arguments: the session, the download task, the number of bytes read since the last time the download progress block was called, the total bytes read, and the total bytes expected to be read during the request, as initially determined by the expected content size of the `NSHTTPURLResponse` object. This block may be called multiple times, and will execute on the session manager operation queue.
*/
- (void)setDownloadTaskDidWriteDataBlock:(nullable void (^)(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, int64_t bytesWritten, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite))block;
/**
Sets a block to be executed when a download task has been resumed, as handled by the `NSURLSessionDownloadDelegate` method `URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:`.
@param block A block object to be executed when a download task has been resumed. The block has no return value and takes four arguments: the session, the download task, the file offset of the resumed download, and the total number of bytes expected to be downloaded.
*/
- (void)setDownloadTaskDidResumeBlock:(nullable void (^)(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, int64_t fileOffset, int64_t expectedTotalBytes))block;
使用:
[_session setDownloadTaskDidWriteDataBlock:^(NSURLSession * _Nonnull session, NSURLSessionDownloadTask * _Nonnull downloadTask, int64_t bytesWritten, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite) {
NSLog(@"下載進度 %lld/%lld",totalBytesWritten,totalBytesExpectedToWrite);
}];
[_session setDownloadTaskDidResumeBlock:^(NSURLSession * _Nonnull session, NSURLSessionDownloadTask * _Nonnull downloadTask, int64_t fileOffset, int64_t expectedTotalBytes) {
NSLog(@"重啟下載 %lld %lld",fileOffset,expectedTotalBytes);
}];
而AF是是如何處理下載進度監聽的呢?
(1)綁定下載進度的回調
上述1.1 初始化task中:[self setDelegate:delegate forTask:downloadTask];實現了對下載進度的監聽,我們看看有關下載部分的具體代碼:
- (void)setDelegate:(AFURLSessionManagerTaskDelegate *)delegate
forTask:(NSURLSessionTask *)task
{
...
[delegate setupProgressForTask:task];
...
}
- (void)setupProgressForTask:(NSURLSessionTask *)task {
__weak __typeof__(task) weakTask = task;
self.downloadProgress.totalUnitCount = task.countOfBytesExpectedToReceive;
[self.downloadProgress setCancellable:YES];
// 1.下載取消的回調
[self.downloadProgress setCancellationHandler:^{
__typeof__(weakTask) strongTask = weakTask;
[strongTask cancel];
}];
// 2.下載暫停的回調
[self.downloadProgress setPausable:YES];
[self.downloadProgress setPausingHandler:^{
__typeof__(weakTask) strongTask = weakTask;
[strongTask suspend];
}];
if ([self.downloadProgress respondsToSelector:@selector(setResumingHandler:)]) {
[self.downloadProgress setResumingHandler:^{
__typeof__(weakTask) strongTask = weakTask;
[strongTask resume];
}];
}
// 1.kvo的方式監聽task countOfBytesReceived,countOfBytesExpectedToReceive屬性的變化。
[task addObserver:self
forKeyPath:NSStringFromSelector(@selector(countOfBytesReceived))
options:NSKeyValueObservingOptionNew
context:NULL];
[task addObserver:self
forKeyPath:NSStringFromSelector(@selector(countOfBytesExpectedToReceive))
options:NSKeyValueObservingOptionNew
context:NULL];
// 2.kvo的方式監聽fractionCompleted即下載進度的變化。
[self.downloadProgress addObserver:self
forKeyPath:NSStringFromSelector(@selector(fractionCompleted))
options:NSKeyValueObservingOptionNew
context:NULL];
}
(2)具體進度變化的觸發
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([object isKindOfClass:[NSURLSessionTask class]] || [object isKindOfClass:[NSURLSessionDownloadTask class]]) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesReceived))]) {
// 1.收到task countOfBytesReceived屬性變化時更新downloadProgress的下載完成的總數
self.downloadProgress.completedUnitCount = [change[NSKeyValueChangeNewKey] longLongValue];
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesExpectedToReceive))]) {
// 2.收到task countOfBytesExpectedToReceive屬性變化時更新downloadProgress的下載總數
self.downloadProgress.totalUnitCount = [change[NSKeyValueChangeNewKey] longLongValue];
}
}
else if ([object isEqual:self.downloadProgress]) {
// 3.收到downloadProgressBlock fractionCompleted屬性變化時觸發上層傳入的下載進度的回調
if (self.downloadProgressBlock) {
self.downloadProgressBlock(object);
}
}
}
1.4 下載暫停,取消
在1.3 downloadProgress中我們看到了下載暫停與取消的回調的設置,我們看看如何觸發回調,先看外部獲取task的downloadProgress實例的接口:
/**
Returns the download progress of the specified task.
@param task The session task. Must not be `nil`.
@return An `NSProgress` object reporting the download progress of a task, or `nil` if the progress is unavailable.
*/
- (nullable NSProgress *)downloadProgressForTask:(NSURLSessionTask *)task;
拿到了task對應的downloadProgress的實例,就可以執行cancel
,pause
的方法,觸發downloadProgress設置的回調,進而改變task的狀態,具體調用實例:
[[session downloadProgressForTask:downloadTask] pause];
[[session downloadProgressForTask:downloadTask] cancel];
當然也可以直接操作task:
[_downloadTask suspend];
[_downloadTask resume];
2.關于斷點續傳
網絡下載中難免不需要考慮斷點續傳的問題。
2.1 非退出程序的斷點續傳
這種需求,原生的Api支持,且同時也支持后臺下載,具體看demo代碼:
typedef void (^DownloadProgressBlock)(NSProgress *downloadProgress);
typedef NSURL* (^DownloadDestinationBlock)(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response);
typedef void (^DownloadCompletionBlock)(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error);
- (void)initUI{
UIButton *startButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 100, 40)];
startButton.center = CGPointMake(self.view.center.x, 100);
startButton.backgroundColor = [UIColor redColor];
[startButton setTitle:@"開始" forState:UIControlStateNormal];
[startButton addTarget:self action:@selector(start:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:startButton];
UIButton *pauseButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 100, 40)];
pauseButton.center = CGPointMake(self.view.center.x, 200);
pauseButton.backgroundColor = [UIColor redColor];
[pauseButton setTitle:@"停止" forState:UIControlStateNormal];
[pauseButton addTarget:self action:@selector(cancel:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:pauseButton];
}
- (void)cancel:(id)sender{
NSLog(@"下載取消");
__weak typeof(self) weakSelf = self;
[_downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
weakSelf.resumeData = resumeData;
}];
}
- (void)start:(id)sender{
NSLog(@"下載開始");
DownloadProgressBlock downloadProgressBlock = ^(NSProgress * _Nonnull downloadProgress) {
CGFloat progress = 1.0 * downloadProgress.completedUnitCount / downloadProgress.totalUnitCount;
NSLog(@"下載進度 :%.2f",progress);
};
DownloadDestinationBlock downloadDestinationBlock = ^NSURL*(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
// 下載文件存儲的路徑
NSString * path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
path = [path stringByAppendingPathComponent:response.suggestedFilename];
NSLog(@"下載路徑 :%@",path);
return [NSURL fileURLWithPath:path];
};
DownloadCompletionBlock downloadCompletionBlock = ^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
if(!error){
NSLog(@"下載完成: %@",[filePath path]);
}
};
if(_resumeData && _resumeData.length > 0){
self.downloadTask = [_session downloadTaskWithResumeData:_resumeData
progress:downloadProgressBlock
destination:downloadDestinationBlock
completionHandler:downloadCompletionBlock];
}else{
self.session = [AFHTTPSessionManager manager];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"https://dn-arnold.qbox.me/Dash.zip"]];
self.downloadTask = [_session downloadTaskWithRequest:request
progress:downloadProgressBlock
destination:downloadDestinationBlock
completionHandler:downloadCompletionBlock];
}
[_downloadTask resume];
}
運行結果:
2017-06-03 17:38:02.678 InterView[46838:517522] 下載開始
2017-06-03 17:38:03.007 InterView[46838:517748] 下載進度 :0.01
2017-06-03 17:38:03.070 InterView[46838:517738] 下載進度 :0.01
2017-06-03 17:38:03.091 InterView[46838:517743] 下載進度 :0.02
2017-06-03 17:38:03.139 InterView[46838:517738] 下載進度 :0.02
2017-06-03 17:38:03.212 InterView[46838:517743] 下載進度 :0.03
2017-06-03 17:38:03.277 InterView[46838:517743] 下載進度 :0.03
2017-06-03 17:38:03.358 InterView[46838:517738] 下載進度 :0.04
2017-06-03 17:38:03.461 InterView[46838:517738] 下載進度 :0.04
2017-06-03 17:38:03.532 InterView[46838:517748] 下載進度 :0.05
2017-06-03 17:38:03.552 InterView[46838:517522] 下載取消
2017-06-03 17:38:05.285 InterView[46838:517522] 下載開始
2017-06-03 17:38:05.637 InterView[46838:517738] 下載進度 :0.05
2017-06-03 17:38:05.637 InterView[46838:517738] 下載進度 :0.05
2017-06-03 17:38:05.638 InterView[46838:517738] 下載進度 :0.05
2017-06-03 17:38:05.718 InterView[46838:517743] 下載進度 :0.05
2017-06-03 17:38:05.742 InterView[46838:517801] 下載進度 :0.06
2017-06-03 17:38:05.759 InterView[46838:517768] 下載進度 :0.06
2017-06-03 17:38:05.804 InterView[46838:517748] 下載進度 :0.06
2017-06-03 17:38:05.899 InterView[46838:517738] 下載進度 :0.07
2017-06-03 17:38:05.964 InterView[46838:517738] 下載進度 :0.07
2017-06-03 17:38:06.035 InterView[46838:517738] 下載進度 :0.08
2017-06-03 17:38:06.106 InterView[46838:517743] 下載進度 :0.08
2017-06-03 17:38:06.172 InterView[46838:517801] 下載進度 :0.09
2017-06-03 17:38:06.236 InterView[46838:517768] 下載進度 :0.09
2017-06-03 17:38:06.300 InterView[46838:517522] 下載取消
2.2 退出程序的斷點續傳
具體的業務中存在用戶退出程序,重啟進入App依舊下載任務依舊能斷點續傳,如果需要實現這種場景就必須存在臨時的文件存儲,以便重啟讀取數據,查看了相關的Api目前尚未找到實現方式。查看了網上的資料參考了:使用NSURLSession程序退出后繼續下載中的的思想實現了這一需求,寫了一份Demo代碼測試一下:
- (void)cancel:(id)sender{
NSLog(@"下載取消");
[_downloadTask cancel];
}
- (void)start:(id)sender{
NSLog(@"下載開始");
DownloadProgressBlock downloadProgressBlock = ^(NSProgress * _Nonnull downloadProgress) {
dispatch_async(dispatch_get_main_queue(), ^{
CGFloat progress = 1.0 * downloadProgress.completedUnitCount / downloadProgress.totalUnitCount;
NSString *progressString = [NSString stringWithFormat:@"下載進度 :%.2f",progress];
UILabel *progressLabel = [self.view viewWithTag:1025];
progressLabel.text = progressString;
});
};
DownloadDestinationBlock downloadDestinationBlock = ^NSURL*(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
// 下載文件存儲的路徑
NSString * path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
path = [path stringByAppendingPathComponent:response.suggestedFilename];
NSLog(@"下載路徑 :%@",path);
return [NSURL fileURLWithPath:path];
};
DownloadCompletionBlock downloadCompletionBlock = ^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
if(!error){
[[NSUserDefaults standardUserDefaults] removeObjectForKey:_currentDownloadUrl];
[[NSUserDefaults standardUserDefaults] synchronize];
NSLog(@"下載完成: %@",[filePath path]);
}
};
self.currentDownloadUrl = @"https://dn-arnold.qbox.me/Dash.zip";
self.session = [AFHTTPSessionManager manager];
NSData *resumeData;
NSString *resumeDataFilePath = [[NSUserDefaults standardUserDefaults] objectForKey:_currentDownloadUrl];
if(resumeDataFilePath && resumeDataFilePath.length > 0){
if([[NSFileManager defaultManager] fileExistsAtPath:resumeDataFilePath]){
resumeData = [self resumeDataFromFilePath:resumeDataFilePath];
}
}
if(resumeData && resumeData.length > 0){
self.downloadTask = [_session downloadTaskWithResumeData:resumeData
progress:downloadProgressBlock
destination:downloadDestinationBlock
completionHandler:downloadCompletionBlock];
}else{
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:_currentDownloadUrl]];
self.downloadTask = [_session downloadTaskWithRequest:request
progress:downloadProgressBlock
destination:downloadDestinationBlock
completionHandler:downloadCompletionBlock];
resumeDataFilePath = [self resumeDataFilePathFor:_downloadTask
downloadUrl:_currentDownloadUrl];
[[NSUserDefaults standardUserDefaults] setObject:resumeDataFilePath
forKey:_currentDownloadUrl];
[[NSUserDefaults standardUserDefaults] synchronize];
}
[_downloadTask resume];
}
// 拼接斷點續傳初始化傳入的resumeData數據
- (NSData *)resumeDataFromFilePath:(NSString *)filePath{
NSData *resumeData=[[NSData alloc] initWithContentsOfFile:filePath];
if(resumeData && resumeData.length>0){
NSMutableDictionary *resumeDataDict = [NSMutableDictionary dictionary];
NSMutableURLRequest *newResumeRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:_currentDownloadUrl]];
[newResumeRequest addValue:[NSString stringWithFormat:@"bytes=%ld-",resumeData.length] forHTTPHeaderField:@"Range"];
NSData *newResumeRequestData = [NSKeyedArchiver archivedDataWithRootObject:newResumeRequest];
[resumeDataDict setObject:_currentDownloadUrl
forKey:@"NSURLSessionDownloadURL"]; // 需要這一句不然會報錯Code=-1002 "unsupported URL"
[resumeDataDict setObject:[NSNumber numberWithInteger:resumeData.length]
forKey:@"NSURLSessionResumeBytesReceived"];
[resumeDataDict setObject:newResumeRequestData
forKey:@"NSURLSessionResumeCurrentRequest"];
[resumeDataDict setObject:[filePath lastPathComponent]
forKey:@"NSURLSessionResumeInfoTempFileName"];
NSData *resumeData = [NSPropertyListSerialization
dataWithPropertyList:resumeDataDict
format:NSPropertyListBinaryFormat_v1_0
options:0
error:nil];
return resumeData;
}
return nil;
}
// 利用runtime的方式根據task獲取相應的臨時下載的目錄
- (NSString *)resumeDataFilePathFor:(NSURLSessionDownloadTask *)downloadTask
downloadUrl:(NSString *)downloadUrl{
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList([downloadTask class], &outCount);
for (i = 0; i<outCount; i++) {
objc_property_t property = properties[i];
const char* char_f =property_getName(property);
NSString *propertyName = [NSString stringWithUTF8String:char_f];
if ([@"downloadFile" isEqualToString:propertyName]) {
id propertyValue = [downloadTask valueForKey:(NSString *)propertyName];
unsigned int downloadFileoutCount, downloadFileIndex;
objc_property_t *downloadFileproperties = class_copyPropertyList([propertyValue class], &downloadFileoutCount);
for (downloadFileIndex = 0; downloadFileIndex < downloadFileoutCount; downloadFileIndex++) {
objc_property_t downloadFileproperty = downloadFileproperties[downloadFileIndex];
const char* downloadFilechar_f =property_getName(downloadFileproperty);
NSString *downloadFilepropertyName = [NSString stringWithUTF8String:downloadFilechar_f];
if([@"path" isEqualToString:downloadFilepropertyName]){
id downloadFilepropertyValue = [propertyValue valueForKey:(NSString *)downloadFilepropertyName];
if(downloadFilepropertyValue){
return downloadFilepropertyValue;
}
break;
}
}
free(downloadFileproperties);
}else {
continue;
}
}
free(properties);
return nil;
}
上述代碼實現比較粗糙,只是實現核心內容,demo測試基本達到了效果。
存在問題
- 因為是內部實現,蘋果一旦替換了實現方式,或者更改了task的部分屬性名,兼容性會是問題。