iOS NSURLSession 后台下載


iOS NSURLSession 后台下載

蘋果在iOS7 推出了NSURLSession來處理網絡請求,來替代NSURLConnection,並且NSURLSession提供了NSURLSessionTask,它只是一個抽象類,它有兩個子類供開發者使用,分別是NSURLSessionDataTask與NSURLSessionDownloadTask。其中NSURLSessionDownloadTask支持后台下載功能。 根據我的了解,目前華龍和世紀采用了NSURLSessionDataTask替代原有的NSURLConnection處理了H5下載任務,雖然可以短時間處理鎖屏中斷下載的任務,但是網絡環境不好,或者用戶退到后台,下載任務還是會中斷。因此我個人使用NSURLSessionDownloadTask來解決目前的問題。

我們先看一下H5下載流程圖

現有H5下載流程圖

image

添加后台下載H5下載流程圖

image

添加后台下載優化的代碼實現

初始化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;
}

開始執行下載任務

  1. 傳入下載路徑以及文件下載保存路徑,開始下載
 /*
 * 開始下載文件
 * @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];
}
  1. 屆時會執行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以前為例)

更多關於iOS 本地通知可參考本文章

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;
}

參考資料:

使用NSURLSession的簡書

NSURLSessionDownloadTask的深度斷點續傳

iOS使用NSURLSession進行下載(包括后台下載,斷點下載)

撰稿人:楊金銘 jinming.yang@inin88.com image


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM