NSURLSession 实现断点下载详解+Realm存储简介

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

你可能感兴趣的:(NSURLSession 实现断点下载详解+Realm存储简介)