IOS7苹果添加了NSURLSession后为断点下载提供了很大的支持。在NSURLConnection 时期,断点下载需要做很多工作,包括文件流写入,获取文件大小上传给服务器对应的range,进程退到后台后下载继续执行等等,也许苹果考虑到了这些,所以在NSURLSession专门为这些需求提供了支持。
本篇文章主要介绍NSURLSession对于下载的支持和Realm存储简介,随后会贴出NSURLConnection的断点下载方式,不作具体解释。开搞!!
首先NSURLSession 所有的功能都是基于任务即task,每个请求都会有一个task来管理。NSURLSession 为我们提供了三种task:
NSURLSessionDownloadTask,NSURLSessionUploadTask,NSURLSessionDataTask .NSURLSessionDownloadTask 是专门为下载提供的,本篇也是着重使用NSURLSessionDownloadTask,当然NSURLSessionDataTask也可以做断点下载,方式和NSURLConnection类似。
首先NSURLSessionDownloadTask 需要对应的一个代理NSURLSessionDownloadDelegate包含两个方法如下
@protocol NSURLSessionDownloadDelegate
/*
下完完成后会调用你这个方法,location即为系统为我们保存的文件地址,此
时只要将文件移动到自定义的文件夹即可。location的地址打印一下可知是沙
盒目录“Library/Caches/com.apple.nsurlsessiond/Downloads/cn.gr.LFDownloadDemo/CFNetworkDownload_Y0yyNr.tmp”
*/
- (void)URLSession:(NSURLSession*)session downloadTask:(NSURLSessionDownloadTask*)downloadTask
didFinishDownloadingToURL:(NSURL*)location;
@optional
/* 下载过程中会不断调用这个方法,我们可以获取文件大小和下载进度,[demo](https://github.com/wlfiou/LFDownLoadManager)中通过通知将过程通知显示层 */
- (void)URLSession:(NSURLSession*)session downloadTask:(NSURLSessionDownloadTask*)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite;
实现断点下载,主要依赖于下面的方法
/* 通过resumeData来记录已下载文件的情况,可通过序列化打印出具体内容。这里需要解释一下,可能刚接触的同学会把resumeData 理解为已下载的文件,这里并不是 。resumeData只是记录已下载的文件的情况,而已下载的文件,NSURLSession为我们存到了,temp文件中,不用我们来处理。*/
- (NSURLSessionDownloadTask*)downloadTaskWithResumeData:(NSData*)resumeData;
怎么拿到上述的resumeData呢,demo的思路是这样的
情况一.进程没有被退出,只是点击了暂停,这时会调用
- (void)cancelByProducingResumeData:(void(^)(NSData*_Nullable resumeData))completionHandler;
可见block的参数即为我们需要的,只要保存resumeData,下次点击继续的时候用resumeData新建任务即可继续上次下载,
情况二.进程在下载的时候被退出,此时需要我们在AppDelegate做一些处理如下代码
- (void)applicationDidEnterBackground:(UIApplication *)application
{
// 实现如下代码,才能使程序处于后台时被杀死,调用applicationWillTerminate:方法
[[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^(){}];
}
这样在退出的时候会调用下面的方法
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{
if (error && [error.localizedDescription isEqualToString:@"cancelled"]) {
return;
}
LFDownLoadModel *model = [[LFDownLoadDatabaseManager shareManager] getModelWithUrl:task.taskDescription];
// 下载时,进程杀死,重新启动,回调错误
if (error && [error.userInfo objectForKey:NSURLErrorBackgroundTaskCancelledReasonKey]) {
[[LFDownLoadDatabaseManager shareManager] transactionWithBlock:^{
model.state = LFDownloadStateWaiting;
}];
model.resumeData = [error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData];
[model writeDataToLocalPath:model.resumeData];
return ;
}
if (error) {
[[LFDownLoadDatabaseManager shareManager] transactionWithBlock:^{
model.state = LFDownloadStateError;
}];
model.resumeData = [error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData];
[model writeDataToLocalPath:model.resumeData];
return ;
}else{
[[LFDownLoadDatabaseManager shareManager] transactionWithBlock:^{
model.state = LFDownloadStateFinish;
}];
}
if (_currentCount) {
_currentCount--;
[self.dataTaskDic removeObjectForKey:model.url];
}
[self startDownloadWaitingTask];
NSLog(@"\n 文件:%@,下载完成 \n 本地路径:%@ \n 错误:%@ \n", model.fileName, model.localPath, error);
}
这样可以拿到此时的resumeData,demo中保存到了本地,再次打开程序时,点击开始,会从本地获取已经存取好的data 按照上述步骤构建task即可。
Realm
demo存储数据选择的是Reaml,不太了解Realm 的同学,也可以参照demo 的用法进行简单的了解。
这里简单介绍一下,Realm的使用方法。
首先Realm的初始化方法有两种:
一.使用默认的初始化如下
使用该方法的话,资源存储位置为默认的Documents下面的default.realm
+ (instancetype)defaultRealm;
使用的时候只需调用 [RLMRealm defaultRealm]即可,不需要自己再设计单例
二.自定义Configuration (demo采用此种方式)
详情可见LFDataBase文件
+(RLMRealmConfiguration *)config{
static RLMRealmConfiguration *_config ;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_config = [[RLMRealmConfiguration alloc]init];
NSString *path = [LFUtil DocumentDirectory];
_config.deleteRealmIfMigrationNeeded = YES;
NSString *loadPath = [path stringByAppendingPathComponent:@"LFDownload"];
BOOL isRE = [[NSFileManager defaultManager] fileExistsAtPath:loadPath];
if (!isRE) {
[[NSFileManager defaultManager] createDirectoryAtPath:loadPath withIntermediateDirectories:YES attributes:nil error:nil];
}
NSString *downloadDB = [loadPath stringByAppendingPathComponent:@"downloadDB.realm"];
_config.fileURL = [NSURL URLWithString:downloadDB];
});
return _config ;
}
+(RLMRealm *)db{
RLMRealm *realm = [RLMRealm realmWithConfiguration:self.config error:nil];
return realm;
}
Realm不可跨线程使用资源,即单线程查出来的资源,需要做一下转换才能使用,demo里面做了一下copy生成新的model进行操作详情可见LFDownLoadModel。Realm中每个Model都是一个表单,Model继承RLMObject即可,同时需要存储的字段不再需要修饰词修饰Realm会为我们管理。
Realm摒弃了复杂的sql语句,只需要像平时使用谓词那样,进行查找如下代码
NSPredicate *pred = [NSPredicate predicateWithFormat:@"state = %d",LFDownloadStateWaiting];
results = [[LFDownLoadModel objectsInRealm:real withPredicate:pred ] sortedResultsUsingKeyPath:@"lastStateTime" ascending:YES];//递增
修改也很方便,在事务中执行赋值即可修改如下代码
[[LFDownLoadDatabaseManager shareManager] transactionWithBlock:^{
model.state = LFDownloadStateFinish;
}];
需要注意的是,Realm存储数据的大小最高为16M,所以资源最好存在文件中,Realm只存储地址就好。
Realm的高效体现在大量数据操作的时候,相比于sqllite 效率提升的很多,而且比coreData更加轻量易用,demo只是简单使用,有兴趣同学可以深入研究。demo