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