前言

最近还在跟着Coding-iOS源码学习iOS开发,现在来介绍一下Coding-iOS的文件下载和上传机制。其实说实话,在写这文章的时候我对这个机制的总体把握也不是很全面;下面我将通过源码再来熟悉一遍这个全过程。这篇文章主要介绍的是下载流程。

源码先行

Coding_FileManager

Coding-iOS中将文件下载与上传有关的方法都封装在Coding_FileManager.hCoding_FileManager.m文件里面。先来看看其中的主要代码👇
Coding_FileManager里面包含3个类:Coding_FileManager类,Coding_DownloadTask类和Coding_UploadTask类。其中的Coding_UploadTask类与上传有关,这里不做介绍。

Coding_DownloadTask

Coding_DownloadTask类用来封装每一个下载任务,它包含一个下载任务,下载进度和当前下载的文件名,代码如下👇

1
2
3
4
5
6
7
8
@interface Coding_DownloadTask : NSObject
@property (nonatomic, strong) NSURLSessionDownloadTask *task; // 下载任务
@property (nonatomic, strong) NSProgress *progress; // 下载进度
@property (nonatomic, strong) NSString *fileName; // 下载文件名

+ (Coding_DownloadTask *)cDownloadTaskWith:(NSURLSessionDownloadTask *)task progress:(NSProgress *)progress fileName:(NSString *)fileName; // 初始化函数
- (void)cancel; // 取消当前下载函数
@end

他的两个方法的实现也很简单:👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+ (Coding_DownloadTask *)cDownloadTaskWith:(NSURLSessionDownloadTask *)task progress:(NSProgress *)progress fileName:(NSString *)fileName {
Coding_DownloadTask *cDownloadTask = [Coding_DownloadTask alloc] init];
cDownloadTask.task = task;
cDownloadTask.progress = progress;
cDownloadTask.fileName = fileName;

return cDownloadTask;
}

- (void)cancel {
if (_task && (_task.state == NSURLSessionTaskStateRunning || _task.state == NSURLSessionTaskStateSuspended)) {
[_task cancel]; // 先判断task的state, 如果可以取消, 那就取消
}
}

Coding_FileManager

这个类是文件下载上传机制里面的最最最核心的类,好好来看看👇

  1. 类的声明
    1
    2
    3
    4
    5
    6
    7
    8
    9
    @interface Coding_FileManager : NSObject
    + (Coding_FileManager *)sharedManager; // 单例模式
    + (AFURLSessionManager *)af_manager;
    - (AFURLSessionManager *)af_manager;

    + (Coding_DownloadTask *)cDownloadTaskForKey:(NSString *)storage_key; // 根据特定的key获取特定的Coding_DownloadTask
    + (NSURL *)diskDownloadUrlForKey:(NSString *)storage_key; // 暂时不明
    - (Coding_DownloadTask *)addDownloadTaskForObj:(id)obj completionHandler:(void (^)(NSURLResponse *response, NSURL *filePath, NSError *error))completionHandler; // 对于新文件,新建下载任务
    @end
  2. 类的实现
  • 属性
    1. 1个监控文件夹变化的属性:docDownloadWatcher,它是一个DirectoryWatcher对象,用来监控APP下载文件存放的文件夹的动态
    2. 2个字典,downloadDict和diskDownloadDict。downloadDict用来存放当前的下载任务(Coding_DownloadTask),它的key是storage_key;diskDownloadDict用来存放下载的文件的路径URL,它的key也是storage_key
    3. 1个NSURL,downloadDirectory, 用来存放下载文件的文件夹的路径URL
  • 方法
    1. sharedManager
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      // 单例模式初始化Coding_FileManager
      + (Coding_FileManager *)sharedManager {
      static Coding_FileManager *manager = nil;
      static dispatch_once_t onceToken;
      dispatch_once(&onceToken, ^{
      manager = [Coding_FileManager alloc] init];
      // 创建存放下载文件的文件夹
      [manager urlForDownloadFolder];
      });

      return manager;
      }
    2. init
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      // 初始化
      - (instancetype)init {
      self = [super init];
      if (self) {
      [[self class] createFolder:[[self class] downloadPath]]; // 创建下载目录
      _downloadDict = [NSMutableDictionary alloc] init]; // 初始化存放下载任务的字典
      _diskDownloadDict = [NSMutableDictionary alloc] init]; //
      _downloadDirectoryURL = nil; //初始化下载目录的文件夹URL
      _docDownloadWatcher = [DirectoryWatcher watchFolderWithPath:[[self class] downloadPath] delegate:self]; // 添加文件夹状态监控器
      [self directoryDidChange:_docDownloadWatcher]; // 初始化的时候先来之行一遍监控程序
      }

      return self;
      }
    3. urlForDownloadFolder
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      // 创建存放下载图片的文件夹
      - (NSURL *)urlForDownloadFolder {
      if (!_downloadDirectoryURL) {
      if ([self class] createFolder:[[self class] downloadPath]) { // 创建存放下载文件的文件夹
      // 文件夹创建成功
      _downloadDirectoryURL = [NSURL fileURLWithPath:[[self class] downloadPath] isDirectory:YES];
      } else {
      // alert create folder error
      }
      }

      return _downloadDirectoryURL;
      }
    4. downloadPath
      1
      2
      3
      4
      5
      6
      // 得到下载目录文件夹的路径, 该APP的Document目录下面的Coding_Download文件夹
      + (NSString *)downloadPath {
      NSString *documentPath = [NSSearchPathForDirecrotiesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
      NSString *downloadPath = [documentPath stringByAppendingPathComponent:@"Coding_Download"];
      return downloadPath;
      }
    5. createFolder
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      // 创建文件夹
      + (BOOL) createFolder:(NSString *)path {
      BOOL isDir = NO;
      NSFileManager *fileManager = [NSFileManager defaultManager];
      BOOL existed = [fileManager fileExistsAtPath:path isDirectory:&isDir];
      BOOL isCreated = NO;
      if (!(isDir == YES && existed == YES)) {
      isCreated = [fileManager createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil];
      } else {
      isCreated = YES;
      }

      return isCreated;
      }
    6. cDownloadTaskForKey
      1
      2
      3
      4
      5
      6
      7
      8
      + (Coding_DownloadTask *)cDownloadTaskForKey:(NSString *)storage_key {
      if (!storage_key) {
      return nil;
      }

      // 当前的每个下载任务 Coding_DownloadTask 都存放在 _downloadDict 里面,对应的key是storage_key
      return [self sharedManager].downloadDict objectForKey:storage_key];
      }
    7. directoryDidChange:(DirectoryWatcher *)folderWatcher
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      // DirectoryWatcher的delegate, 当文件夹里面的文件变动时触发
      - (void)directoryDidChange:(DirectoryWatcher *)folderWatcher {
      NSMutableDictionary *diskDict = _diskDownloadDict;
      NSString *path = [[self class] downloadPath];
      BOOL isDownload = YES;

      /* 先移除diskDict里面的所有文件URL,再将当前下载目录里面的所有文件URL添加到diskDict里面 */
      [diskDict removeAllObjects];
      NSArray *fileContents = [NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:NULL]; // 获取path文件夹下面的所有内容,将所有文件名存放在fileContents数组里面
      for (NSString *curFileName in [fileContents objectEnumerator]) { // 遍历文件名
      NSString *filePath = [path stringByAppendingPathComponent:curFileName]; // 当前文件的完整的路径
      NSURL *fileUrl = [NSURL fileURLWithPath:filePath]; // 当前文件的URL
      BOOL isDirectory;
      [NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDirectory]; // 判断当前文件是否是文件夹
      if (!isDirectory) { // 如果不是文件夹, 存入diskDict
      NSString *keyStr = [curFileName componentsSeparatedByString:@"|"].lastObject; // 因为下载文件名命名为'IMG_0002.JPG|||264314|||QiniuStorage|c09bd7ea-6be9-431c-afc1-884293895719.JPG', 最后的|后面是storage_key
      [diskDict setObject:fileUrl forKey:keyStr];
      }
      }
      }
    8. addDownloadTaskForObj:completionHandler
      1
      2
      3
      4
      5
      6
      7
      8
      // 新建下载任务
      - (void)addDownloadTaskForObj:(id)obj completionHandler:(void (^)(NSURLResponse *response, NSURL *filePath, NSError *error))completionHandler {
      Coding_DownloadTask *cTask = nil;
      ProjectFile *file = (ProjectFile *)obj; // Coding-iOS中的一个model
      cTask = [self addDownloadTaskWithPath:file.downloadPath diskFileName:fileDiskFileName storage_key:file.storage_key completionHandler:completionHandler];

      return cTask;
      }
    9. addDownloadTaskWithPath:diskFileName:storage_key:completionHandler
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      // 新建下载任务 具体的
      - (void)addDownloadTaskWithPath:(NSString *)downloadPath diskFileName:(NSString *)diskFileName storage_key:(NSString)storage_key completionHandler:(void (^)(NSURLResponse *response, NSURL *filePath, NSError *error))completionHandler {
      NSProgress *progress;
      NSURL *downloadPath = [NSURL URLWithString:downloadPath];
      NSURLRequest *request = [NSURLRequest requestWithURL:downloadPath];
      NSURLSessionDownloadTask *downloadTask = [self.af_manager downloadTaskWithRequest:request progress:&progress destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
      NSURL *downloadURL = [Coding_FileManager sharedManager] urlForDownloadFolder];
      Coding_DownloadTask *cDownloadTask = [Coding_FileManager cDownloadTaskForResponse:response];
      if (cDownloadTask) {
      downloadUrl = [downloadUrl URLByAppendingPathComponent:cDownloadTask.diskFileName];
      } else {
      downloadUrl = [downloadUrl URLByAppendingPathComponent:[response suggestedFileName]];
      }

      return downloadUrl; // 返回存储的目标位置
      } completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
      if (error) {
      [Coding_FileManager cancelCDownloadTaskForKey:storage_key];
      } else {
      [Coding_FileManager cancelCDownloadTaskForResponse:response];
      }
      }];

      Coding_DownloadTask *cDownloadTask = [Coding_DownloadTask cDownloadTaskWithTask:downloadTask progress:progress fileName:diskFileName];

      [self.downloadDict setObject:cDownloadTask forKey:storage_key];
      [downloadTask resume];
      return cDownloadTask;
      }

实力分析

源码已经介绍完了,来分析一波。注意下面几个要点👇

  1. 每个下载任务都是一个Coding_DownloadTask类的实例,这个类里面有个NSURLSessionDownloadTask属性
  2. 所有下载有关的操作都会涉及到一个类Coding_FileManager,Coding会创建一个Coding_FileManager的单例类(既:APP生命周期内只会有一个这个类的实例)。该实例有两个字典:1个负责存储已经下载下来的文件的文件URL,另一个负责存储当前还在进行的下载任务(Coding_DownloadTask)。这两个字典的key由后台定义,每个文件/任务对应唯一的一个storage_key
  3. 所有下载的文件存放在APP的Document文件夹下面的Coding_Download文件夹下面,在创建单例类Coding_FileManager的时候检测该文件夹是否存在,不存在就创建。
  4. 开始一个新的下载任务其实就是创建一个NSURLSessionDownloadTask,然后再将该NSURLSessionDownloadTask封装进Coding_DownloadTask。而创建该NSURLSessionDownloadTask用的是AFURLSessionManager的downloadTaskWithRequest方法。
  5. 有一个监控下载文件夹内容变化的监控器,每当文件夹内容变化时(文件被删除或文件下载完成)它会自动更新存放下载文件路径URL的字典,也就是更新当前APP下载了哪些文件

总结

最后来用精炼的语言来总结一番:

  1. 每个下载任务/文件都有唯一与之对应的storage_key。每当新来一个下载任务时,先查看已经完成的文件里面是否包括了该文件(即:查看存放已下载文件的字典是否有该storage_key)。若已经下载,则完成下载任务;还未下载,进行第二步。
  2. 判断该任务是否存在(即:查看存储下载任务的字典,看是否有该storage_key)。若该任务存在,说明之前这个任务可能被暂停了,那么继续该下载任务,并跳过步骤3;若该任务不存在,那么进行第三步。
  3. 根据任务的storage_key,文件的url创建Coding_DownloadTask,其实主要的是创建一个NSURLSessionDownloadTask,然后将该Coding_DownloadTask存放到存储下载任务的字典中,并开始下载文件。
  4. 当文件下载完成时候,从存储下载任务的字典中将这个任务删除;并且文件会被存放到相应的文件夹,触发文件夹监控器的函数,将该文件的文件URL添加到存储已经下载完成的文件URL的字典中。

参考

P.S

已经过了2个大冰红茶了,一立还会远嘛~~~