1、背景
最近发现好多游戏 App,都支持后台资源下载,进入后台的时候下载更新,再次唤醒或者重启动的时候,把直接加载已经下次好的资源,让用户几乎无感知的,提升了用户体验,能够及时使用最新场景,于是做了简单的调研和测试
2、后台任务
Xcode 后台任务配置
开启后台模式,勾选 Background fetch Background processing
- Background Fetch(后台抓取)
允许应用程序在后台定期唤醒并从网络获取新数据,以确保应用程序中的内容保持最新。有系统统一调度,根据用户使用习惯触发,并且执行的时间短(最多 30s)
- Background Processing(后台处理)
使应用程序能够在后台执行一些长时间运行的任务,如文件处理、资源下载,数据计算等。执行时间相对较长,但具体时长也会受到系统资源和设备状态的限制。应用可以在这个时间段内持续执行任务,直到任务完成或系统终止。
选择完上面会在 info.plist 中自动生成
后台 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技术,可以使用每次重新下载资源