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 
