iOS关于文件上传与下载

前言

最近还在跟着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类用来封装每一个下载任务,它包含一个下载任务,下载进度和当前下载的文件名,代码如下👇

        @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

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

        + (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. 类的声明

         @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

             // 单例模式初始化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

             // 初始化
             - (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

             // 创建存放下载图片的文件夹
             - (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
             // 得到下载目录文件夹的路径, 该APP的Document目录下面的Coding_Download文件夹
             + (NSString *)downloadPath {
                 NSString *documentPath = [NSSearchPathForDirecrotiesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
                 NSString *downloadPath = [documentPath stringByAppendingPathComponent:@"Coding_Download"];
                 return downloadPath;
             }
      
    5. createFolder

             // 创建文件夹
             + (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

             + (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

             // 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

             // 新建下载任务
             - (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

             // 新建下载任务 具体的
             - (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个大冰红茶了,一立还会远嘛~~~

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器