iOS NSURLSession 后台下载
苹果在iOS7 推出了NSURLSession来处理网络请求,来替代NSURLConnection,并且NSURLSession提供了NSURLSessionTask,它只是一个抽象类,它有两个子类供开发者使用,分别是NSURLSessionDataTask与NSURLSessionDownloadTask。其中NSURLSessionDownloadTask支持后台下载功能。 根据我的了解,目前华龙和世纪采用了NSURLSessionDataTask替代原有的NSURLConnection处理了H5下载任务,虽然可以短时间处理锁屏中断下载的任务,但是网络环境不好,或者用户退到后台,下载任务还是会中断。因此我个人使用NSURLSessionDownloadTask来解决目前的问题。
我们先看一下H5下载流程图
现有H5下载流程图
添加后台下载H5下载流程图
添加后台下载优化的代码实现
初始化NSURLSession会话
// 生成单例
+ (YT_BackDownManager *)shareBackDownManager {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[YT_BackDownManager alloc] init];
[instance backgroundURLSession];
});
return instance;
}
// 创建并返回支持后台处理的Session会话 单例
- (NSURLSession *)backgroundURLSession {
static NSURLSession *session = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *identifier = @"com.ytgfcompany.appId.BackgroundSession";
NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:identifier];
session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
});
return session;
}
开始执行下载任务
- 传入下载路径以及文件下载保存路径,开始下载
/*
* 开始下载文件
* @param delegate 代理
* @param downloadURLString 文件下载地址
* @param filePath 文件存放路径
*/
- (void)beginDownWithTarget:(id)delegate
downLoadURL:(NSString *)downloadURLString
filePath:(NSString *)filePath {
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
NSLog(@"文件存在,删除文件");
[[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
}
NSURL *downloadURL = [NSURL URLWithString:downloadURLString];
NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL];
NSURLSession *session = [self backgroundURLSession];
self.delegate = delegate;
self.filePath = filePath;
self.downloadTask = [session downloadTaskWithRequest:request];
[self.downloadTask resume];
}
- 届时会执行NSURLSessionDownloadDelegate代理
#pragma mark - NSURLSessionDownloadDelegate
/*
* 下载过程中调用,用于跟踪下载进度
* @param bytesWritten为单次下载大小
* @param totalBytesWritten为当当前一共下载大小
* @param totalBytesExpectedToWrite为文件大小
*/
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
dispatch_async(dispatch_get_main_queue(), ^{
if ([self.delegate respondsToSelector:@selector(fileDownloadingWithFileSize:totalSize:)]) {
[self.delegate fileDownloadingWithFileSize:totalBytesWritten totalSize:totalBytesExpectedToWrite];
}
});
}
/*
* 下载完成调用,并且支持自动保存下载内容到沙盒目录的Cash缓存中,需要手动转移到Documents
* @param session 为当前下载会话
* @param downloadTask 为当前下载任务
* @param location 为默认文件临时存放路径地址
*/
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location {
NSLog(@"downloadTask:%lu didFinishDownloadingToURL:%@", (unsigned long)downloadTask.taskIdentifier, location);
NSString *locationString = [location path];
NSString *finalLocation = _filePath;
NSLog(@"存放路径:%@", finalLocation);
// 用 NSFileManager 将文件复制到应用的存储中
NSError *error;
[[NSFileManager defaultManager] moveItemAtPath:locationString toPath:finalLocation error:&error];
// 通知 UI 刷新
}
/*
* 该方法下载成功和失败都会回调,只是失败的是error是有值的,
* 在下载失败时,error的userinfo属性可以通过NSURLSessionDownloadTaskResumeData
* 这个key来取到resumeData(和上面的resumeData是一样的),再通过resumeData恢复下载
*/
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error {
if (error) {
// check if resume data are available
if ([error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData]) {
NSData *resumeData = [error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData];
//通过之前保存的resumeData,获取断点的NSURLSessionTask,调用resume恢复下载
self.resumeData = resumeData;
if ([self.delegate respondsToSelector:@selector(fileDownResult:fileSize:)]) {
[self.delegate fileDownResult:error fileSize:[resumeData length]];
}
}
} else {
AppDelegate *delegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
[delegate sendLocalNotification];
if ([self.delegate respondsToSelector:@selector(fileDownResult:fileSize:)]) {
[self.delegate fileDownResult:nil fileSize:[[NSData dataWithContentsOfFile:_filePath] length]];
}
}
}
后台下载
当程序退到后台进行下载时,不会再走NSURLSession的代理方法,只有所有的下载任务都执行完成,系统会调用ApplicationDelegate的application:handleEventsForBackgroundURLSession:completionHandler:回调,之后“汇报”下载工作,对于每一个后台下载的Task调用Session的Delegate中的URLSession:downloadTask:didFinishDownloadingToURL:(成功的话)和URLSession:task:didCompleteWithError:(成功或者失败都会调用)
- (void)application:(UIApplication *)application
handleEventsForBackgroundURLSession:(NSString *)identifier
completionHandler:(void (^)(void))completionHandler {
// 必须重新建立一个后台 seesion 的参照
// 否则 NSURLSessionDownloadDelegate 和 NSURLSessionDelegate 方法会因为
// 没有 对 session 的 delegate 设定而不会被调用。参见上面的 backgroundURLSession
NSURLSession *backgroundSession = [[YT_BackDownManager shareBackDownManager] backgroundURLSession];
NSLog(@"Rejoining session with identifier %@ %@", identifier, backgroundSession);
// 保存 completion handler 以在处理 session 事件后更新 UI
[[YT_BackDownManager shareBackDownManager] addCompletionHandler:completionHandler forSession:identifier];
}
我们需要把completionHandler添加保存起来
#pragma mark Save completionHandler
- (void)addCompletionHandler:(CompletionHandlerType)handler
forSession:(NSString *)identifier {
if ([self.completionHandlerDictionary objectForKey:identifier]) {
NSLog(@"Error: Got multiple handlers for a single session identifier. This should not happen.\n");
}
[self.completionHandlerDictionary setObject:handler forKey:identifier];
}
在执行完下载完成的代理方法以后,之后调用Session的Delegate回调URLSessionDidFinishEventsForBackgroundURLSession
/*
* 后台下载完成以后,需要调用唤起下完成要处理的任务
* @param session 为当前下载会话
*/
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
NSLog(@"Background URL session %@ finished events.\n", session);
if (session.configuration.identifier) {
// 调用在 -application:handleEventsForBackgroundURLSession: 中保存的 handler
[self callCompletionHandlerForSession:session.configuration.identifier];
}
}
调用callCompletionHandlerForSession处理下载完成以后需要更新处理的任务工作 ``` - (void)callCompletionHandlerForSession:(NSString *)identifier { CompletionHandlerType handler = [self.completionHandlerDictionary objectForKey:identifier];
if (handler) {
[self.completionHandlerDictionary removeObjectForKey: identifier];
NSLog(@"Calling completion handler for session %@", identifier);
handler();
}
} ```
扩展:关于后台下载完,需要做本地通知(我以iOS 10以前为例)
1.在Appdelegate DidFinishedLaunch进行出初始化本地通知
// 初始化本地通知
- (void)initLocalNotification {
self.localNotification = [[UILocalNotification alloc] init];
self.localNotification.fireDate = [[NSDate date] dateByAddingTimeInterval:5];
self.localNotification.alertAction = nil;
self.localNotification.soundName = UILocalNotificationDefaultSoundName;
self.localNotification.alertBody = @"下载完成了!";
self.localNotification.applicationIconBadgeNumber = 1;
self.localNotification.repeatInterval = 0;
}
2.在Appdelegate DidFinishedLaunch 进行申明本地通知权限
// ios8后,需要添加这个注册,才能得到授权
if ([[UIApplication sharedApplication] respondsToSelector:@selector(registerUserNotificationSettings:)]) {
UIUserNotificationType type = UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound;
UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:type
categories:nil];
[[UIApplication sharedApplication] registerUserNotificationSettings:settings];
// 通知重复提示的单位,可以是天、周、月
self.localNotification.repeatInterval = 0;
} else {
// 通知重复提示的单位,可以是天、周、月
self.localNotification.repeatInterval = 0;
}
UILocalNotification *localNotification = [launchOptions valueForKey:UIApplicationLaunchOptionsLocalNotificationKey];
if (localNotification) {
[self application:application didReceiveLocalNotification:localNotification];
}
3. 当需要发送本地通知的时候,把要发送的本地通知添加到schedule表里,就会收到本地通知了
[[UIApplication sharedApplication] scheduleLocalNotification:self.localNotification];
4. 发送通知以后会有回调,处理回调方法如下
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification {
[[UIApplication sharedApplication] cancelAllLocalNotifications];
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"下载通知"
message:notification.alertBody
delegate:nil
cancelButtonTitle:@"确定"
otherButtonTitles:nil];
[alert show];
// 图标上的数字减1
application.applicationIconBadgeNumber -= 1;
}
参考资料:
NSURLSessionDownloadTask的深度断点续传
iOS使用NSURLSession进行下载(包括后台下载,断点下载)
撰稿人:杨金铭 jinming.yang@inin88.com