系列文章:
在上一章节中我们通过一个具体的实例讲解了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的部分属性名,兼容性会是问题。