iOS关于文件上传与下载
前言
最近还在跟着Coding-iOS源码学习iOS开发,现在来介绍一下Coding-iOS的文件下载和上传机制。其实说实话,在写这文章的时候我对这个机制的总体把握也不是很全面;下面我将通过源码再来熟悉一遍这个全过程。这篇文章主要介绍的是下载流程。
源码先行
Coding_FileManager
Coding-iOS中将文件下载与上传有关的方法都封装在Coding_FileManager.h
和Coding_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
类
这个类是文件下载上传机制里面的最最最核心的类,好好来看看👇
类的声明
@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
- 类的实现
属性
- 1个监控文件夹变化的属性:docDownloadWatcher,它是一个
DirectoryWatcher
对象,用来监控APP下载文件存放的文件夹的动态 - 2个字典,downloadDict和diskDownloadDict。downloadDict用来存放当前的下载任务(Coding_DownloadTask),它的key是storage_key;diskDownloadDict用来存放下载的文件的路径URL,它的key也是storage_key
- 1个NSURL,downloadDirectory, 用来存放下载文件的文件夹的路径URL
- 1个监控文件夹变化的属性:docDownloadWatcher,它是一个
方法
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; }
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; }
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; }
downloadPath
// 得到下载目录文件夹的路径, 该APP的Document目录下面的Coding_Download文件夹 + (NSString *)downloadPath { NSString *documentPath = [NSSearchPathForDirecrotiesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; NSString *downloadPath = [documentPath stringByAppendingPathComponent:@"Coding_Download"]; return downloadPath; }
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; }
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]; }
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]; } } }
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; }
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; }
实力分析
源码已经介绍完了,来分析一波。注意下面几个要点👇
- 每个下载任务都是一个
Coding_DownloadTask
类的实例,这个类里面有个NSURLSessionDownloadTask属性 - 所有下载有关的操作都会涉及到一个类
Coding_FileManager
,Coding会创建一个Coding_FileManager
的单例类(既:APP生命周期内只会有一个这个类的实例)。该实例有两个字典:1个负责存储已经下载下来的文件的文件URL,另一个负责存储当前还在进行的下载任务(Coding_DownloadTask)。这两个字典的key由后台定义,每个文件/任务对应唯一的一个storage_key - 所有下载的文件存放在APP的Document文件夹下面的Coding_Download文件夹下面,在创建单例类Coding_FileManager的时候检测该文件夹是否存在,不存在就创建。
- 开始一个新的下载任务其实就是创建一个NSURLSessionDownloadTask,然后再将该NSURLSessionDownloadTask封装进Coding_DownloadTask。而创建该NSURLSessionDownloadTask用的是AFURLSessionManager的downloadTaskWithRequest方法。
- 有一个监控下载文件夹内容变化的监控器,每当文件夹内容变化时(文件被删除或文件下载完成)它会自动更新存放下载文件路径URL的字典,也就是更新当前APP下载了哪些文件
总结
最后来用精炼的语言来总结一番:
- 每个下载任务/文件都有唯一与之对应的storage_key。每当新来一个下载任务时,先查看已经完成的文件里面是否包括了该文件(即:查看存放已下载文件的字典是否有该storage_key)。若已经下载,则完成下载任务;还未下载,进行第二步。
- 判断该任务是否存在(即:查看存储下载任务的字典,看是否有该storage_key)。若该任务存在,说明之前这个任务可能被暂停了,那么继续该下载任务,并跳过步骤3;若该任务不存在,那么进行第三步。
- 根据任务的storage_key,文件的url创建Coding_DownloadTask,其实主要的是创建一个NSURLSessionDownloadTask,然后将该Coding_DownloadTask存放到存储下载任务的字典中,并开始下载文件。
- 当文件下载完成时候,从存储下载任务的字典中将这个任务删除;并且文件会被存放到相应的文件夹,触发文件夹监控器的函数,将该文件的文件URL添加到存储已经下载完成的文件URL的字典中。
参考
P.S
已经过了2个大冰红茶了,一立还会远嘛~~~