iOS 后台资源下载

79 阅读3分钟

1、背景

最近发现好多游戏 App,都支持后台资源下载,进入后台的时候下载更新,再次唤醒或者重启动的时候,把直接加载已经下次好的资源,让用户几乎无感知的,提升了用户体验,能够及时使用最新场景,于是做了简单的调研和测试

2、后台任务

Xcode 后台任务配置

开启后台模式,勾选 Background fetch  Background processing

  • Background Fetch(后台抓取)

允许应用程序在后台定期唤醒并从网络获取新数据,以确保应用程序中的内容保持最新。有系统统一调度,根据用户使用习惯触发,并且执行的时间短(最多 30s)

  • Background Processing(后台处理)

使应用程序能够在后台执行一些长时间运行的任务,如文件处理、资源下载,数据计算等。执行时间相对较长,但具体时长也会受到系统资源和设备状态的限制。应用可以在这个时间段内持续执行任务,直到任务完成或系统终止。

image.png

选择完上面会在 info.plist 中自动生成

image.png

后台 backgroundSession 下载

NSURLSession后台任务下载,不需要添加上面 Background Modes , 如果你添加了可以不需要选中,Background Fetch 和 Background Processing

后台Session下载系统默认支持,无需过多设置

- (NSURLSession *)bgSession{
    if (!_bgSession) {
        ///需要指定一个唯一标识
        NSURLSessionConfiguration *bgSessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:backgroundId];
        bgSessionConfig.discretionary = YES; // 设置为 YES 让系统调度任务执行时间
        bgSessionConfig.sessionSendsLaunchEvents = YES; // 设置为 YES 允许系统启动应用处理任务完成事件
        bgSessionConfig.allowsCellularAccess = YES; // 设置为 YES 允许使用蜂窝
        bgSessionConfig.timeoutIntervalForResource = 60; // 资源超时时间
        //还有其他属性根据实际情况设置
        _bgSession = [NSURLSession sessionWithConfiguration:bgSessionConfig delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    }
    return _bgSession;
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
    NSLog(@"applicationDidEnterBackground");
    [self backgroudDownLoad];
}

-(void)backgroudDownLoad{
    NSString *urlStr = @"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
    if (!self.resumeData){
          self.resumeData = [[NSUserDefaults standardUserDefaults] valueForKey:@"resume_data"];
      }
    if (self.resumeData.length > 0) { /// 可以实现断点续传
        self.downloadTask = [self.bgSession downloadTaskWithResumeData:self.resumeData];
        NSLog(@"download resumeData");
    }else{
        if(self.downloadTask){
            NSLog(@"download return");
            return;
        }
        self.downloadTask = [self.bgSession downloadTaskWithURL:[NSURL URLWithString:urlStr]];
    }
    
    [self.downloadTask resume];

}
// 先保存completionHandler ,下载完成之后(任务处理完)要立即调用一下 block
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler{
    self.backgroundSessionCompletionHandler = completionHandler;
}

#pragma mark - DownloadDelegate
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSURL *documentDir = [fileManager URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:false error:nil];
    NSURL *destinationUrl = [documentDir URLByAppendingPathComponent:downloadTask.originalRequest.URL.lastPathComponent];
    
    [fileManager removeItemAtURL:destinationUrl error:nil];
    [fileManager copyItemAtURL:location toURL:destinationUrl error:nil];
    NSLog(@"location = %@",location);
    NSLog(@"destinationUrl = %@",destinationUrl);

    /// 处理完任务立即调用 completionHandler
    dispatch_async(dispatch_get_main_queue(), ^{
        if (self.backgroundSessionCompletionHandler) {
            self.backgroundSessionCompletionHandler();
        }
    });
}
/* Sent periodically to notify the delegate of download progress. */
// 在后台这个方法不会调用,只有在前台会调用
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
                                           didWriteData:(int64_t)bytesWritten
                                           totalBytesWritten:(int64_t)totalBytesWritten
                                           totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{
    
    
    float progress =  (totalBytesWritten * 1.0) / totalBytesExpectedToWrite;
    NSLog(@"progress = %.2f",progress);
}

/* Sent when a download has been resumed. If a download failed with an
 * error, the -userInfo dictionary of the error will contain an
 * NSURLSessionDownloadTaskResumeData key, whose value is the resume
 * data.
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
                                             didResumeAtOffset:(int64_t)fileOffset
                                            expectedTotalBytes:(int64_t)expectedTotalBytes{
    NSLog(@"downloadTask");
}


/// 错误处理
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{
    
    NSLog(@"error = %@",error);
    
    if (error && [task isKindOfClass:NSURLSessionDownloadTask.class]) {
        self.resumeData = error.userInfo[NSURLSessionDownloadTaskResumeData];
       [[NSUserDefaults standardUserDefaults] setValue:self.resumeData forKey:@"resume_data"];
        NSLog(@"error_resume_data = %ld",[error.userInfo[NSURLSessionDownloadTaskResumeData] length]);
    }else{
        NSLog(@"downlaod task done");
    }
}

 3.存在问题

  • 上面后台下载不是真正的断点续传,记录的 data 里面包含了 下载相关数据,才能继续下载

  • 如果在下载的过程中,直接 kill app , 不能够获取到失败回调的 data, 这种续传就会有问问题

  • 想要真实的断点续传

    • 服务器需要支持 Range requests(范围请求)是关键技术

    • 客户端也需要添加 Header  Range

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlStr]];
        
        if (self.resumeData > 0) {
            // 设置 Range 请求头
            NSString *rangeHeader = [NSString stringWithFormat:@"bytes=%lld-", (long long)self.resumeData];
            [request setValue:rangeHeader forHTTPHeaderField:@"Range"];
        }

4.结论

  • 可以通过上述方法实现游戏资源后台下载

  • 如服务端不支持 Range技术,可以使用每次重新下载资源