iOS – 使用“AFNetworking”实现一个下载器 (思路)

项目中使用AFN实现一个下载器 (思路,非代码)

在此下载器中,我实现了两个类

  • DownloadTaskModel: 下载任务类
  • DownloadManager: 下载器核心类

DownloadTaskModel: 下载任务类

该类应该包含和下载任务相关的属性,因为下载器属于一个通用的组件,所以不应该包含实际业务代码中的Model,例如视频Model,图片Model,其他资源的Model等,在下载器外部使用实际业务中的Model去封装一个DownloadTaskModel, 这样下载器在使用model时就与业务代码解耦了。

我认为一个最基本的DownloadTaskModel应该包含以下属性。

  • primaryKey: 写入数据库时的主键
  • downloadState: 下载状态 (下载中,暂停,取消,出错等)
  • createdTime: 任务创建时间
  • localUrl: 本地存储位置
  • downloadUrl: 下载地址
  • resumeDataUrl: 临时缓存数据 (比如下载到一半)
  • errorMessage: 错误信息
  • downloadSize: 下载的大小
  • totalSize: 总大小 (一般由外部model赋值,可以比对是否下载完成)

  • session: 可以指向NSURLSessionDownloadTask对象,以便于对下载任务进行操作。

DownloadManager 下载器核心类

下载器核心业务我认为需要包括如下核心功能

  • download: 下载
  • pause: 暂停
  • resume: 恢复已经暂停的任务
  • restart: 重新下载
  • cancel: 取消下载

另外还可以将几个常用的操作封装起来对外开放

  • checkDiskSize: 通过传入的taskModel检查磁盘是否够用
  • delete: 删除已经下载的任务
  • totalCount: 下载总数
  • filePathForKey: 通过传入key来获取文件下载地址
  • taskExist: 判断任务是否已经存在

1. 下载器初始化

下载器应该是一个单例供全局调用,在初始化的时候可以按需读取数据库中存储的下载任务DownloadTaskModel.

因为这个初始化过程一般是在刚打开APP时操作,所以此时可以对现存的下载任务进行检测。

比如某个任务已经存在缓存文件,也就是下载到一半了,可以设置其下载状态为暂停。

如果没有缓存,但是数据库标记正在下载中,说明这个任务在上次使用没有正常进行,可以重新开启下载任务,或者标记下载出错。

完成了对数据的清洗之后,可以重新保存到数据库中。

2. 下载任务

下载任务方法支持从外部传入一个taskModel, 然后下载器将taskModel存入数据库,同时开始下载任务。

在开始下载任务之前,我们可以判断当前正在进行的任务数,如果任务数大于了系统的设定值,则先将下载任务加到等待队列中 (修改taskModel的下载状态,并保存到数据库)。

开始下载任务可以分为两个情况。

  1. 是否已经存在部分缓存文件,如果存在则继续下载;
  2. 从未下载过,开始下载。

在前文研究AFN的时候,我发现AFHTTPSessionManager只提供了常用的GET,POST等网络请求操作,而其父类AFURLSessionManager虽然不执行网络请求,但是可以获取一个经过封装的任务(dataTask, downloadTask, uploadTask)

所以可以使用AFURLSessionManager获取一个下载任务,并实现下载进度,下载完成等block。

下面是一个从无下载过的任务的创建

session = [mgr downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull downloadProgress) {
    // 下载进度管理
    [self handleProgressDownload:downloadProgress downloadTask:downloadTask];
} destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
    // 这是存下视频文件
    return [NSURL fileURLWithPath: @"xxxxx"];
} completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
    // 下载完成,做处理
    [self handleDownloadCompletionResponse:response filePath:filePath error:error task:downloadTask];
}];

下面是一个有部分缓存数据的下载任务的创建

session = [mgr downloadTaskWithResumeData:resumnData progress:^(NSProgress * _Nonnull downloadProgress) {
    [self handleProgressDownload:downloadProgress downloadTask:downloadTask];
} destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
    //这是存下视频文件
    return [NSURL fileURLWithPath: [self filePathForUid:downloadTask.uid]];
} completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
    [self handleDownloadCompletionResponse:response filePath:filePath error:error task:downloadTask];
}];

区别在于一个传入的是网络请求,一个传入的是ResumeData。

NSURLSession类可以通过多种方式生成下载任务

- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request;

- (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url;

- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData;

在下载任务需要暂停时,可以存一个resumeData到缓存文件,想要恢复下载时可以打开该resumeData, 然后通过resumeData生成下载任务,即可恢复下载。

关于断点续传原理:

首先,如果想要进行断点续传,那么需要简单了解一下断点续传的工作机制,在HTTP请求头中,有一个Range的关键字,通过这个关键字可以告诉服务器返回哪些数据给我。
比如:
bytes=500-999 表示第500-第999字节
bytes=500- 表示从第500字节往后的所有字节
然后我们再根据服务器返回的数据,将得到的data数据拼接到文件后面,就可以实现断点续传了。

生成好下载任务后,即可手动开启任务 [session resume]

3. 暂停任务

通过获取DownloadTaskModel里面的session,也就是downlodTask, 然后对task调用cancelByProducingResumeData 可以进行暂停下载。

if (task.session && task.downloadState == CBDownloadStateDownloading) {
        [task.session cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
            if([resumeData writeToFile:[self resumnDataPathForUid:task.uid] atomically:YES]){
                DLog(@"写入Resume data成功");
                task.resumnDataUrl = [self resumnDataNameForUid:task.uid];
                task.downloadState = CBDownloadStatePausing;
                task.session = nil;
                
                [self saveTaskToDB:task];
                [CBNotificationHelper post:kCBDownloadDidPauseNotification withData:@{kDownloadTaskKey: task}];
            }else{
                DLog(@"resume写入失败");
            }
        }];
    }

在暂停下载时,需要我们手动将resumeData文件写到缓存目录,以便于恢复下载任务时使用。

4. 继续下载

这一步和一开始创建下载任务时类似,找到resumeData,然后恢复下载。

5. 重新下载

删掉缓存文件,重置model里面部分具有下载标识的字段,然后重新添加到下载队列即可。

6. 取消下载

取消下载不需要缓存数据,因此直接调用cancel即可

[task.session cancel];

总结:

在本次下载器的设计中,通过为下载器设计一个Model,可以实现与业务代码的解耦。

通过NSURLSession获取一个下载任务,然后手动执行该任务即可。之所以能够断线续传,是因为在请求服务器URL时,可以在头部添加bytes字段表示当前从哪里继续下载,这可能需要服务器的支持。

同时resumeData并不是系统自己生成的,而是我们在取消下载任务时自己去写入到文件夹中的。