前言
项目中有文件上传的需求,目前是单利实现,仅支持单文件上传,并且没有超时,取消等操作,所以考虑扩展下相关的功能。
文件批量上传方案
-
手动创建一个任务队列(数组),控制正在执行的任务数量
开始上传任务时,需要根据生成的文件key(文件路径或者hash值),加入到临时数组中,文件上传结束(失败&成功),从数组中移除这个文件。任务添加到执行队列前,判断是否达到最大数量
-
使用GCD 的
dispatch_semaphore_t来控制任务在执行的数量设置信号量阈值为同时执行的的上传任务最大数量,任务执行前调用
dispatch_semaphore_wait,任务结束调用dispatch_semaphore_signal -
自定义
NSOperation,把operation加入到NSOperationQueue中将任务封装成operation,加入到queue中,同时设置下最大并发数量
方案对比
这三种方案都能实现批量上传的网络资源竞争问题。数组的方式需要处理多线程访问的问题, dispatch_semaphore_t需要维护一个全局的实例,这两种都不支持任务取消操作。
NSOperation 可以很方便的取消任务,处理一些异常情况。所以最终选择NSOperation的方式实现。具体实现也可以参考一些优秀的第三方库对于Operation的封装实现。
NSOperation基本知识
-
状态变更
NSOperation在创建后进入isReady状态方可开始需要执行的任务;
任务执行中进入isExecuting状态;
执行结束后进入isFinished状态,同时如果该NSOperation是在NSOperationQueue中,会从queue中移除;
任务未开始执行前,可以取消NSOperation任务,取消后进入isCancelled状态。被cancel掉的任务是不能执行的,所以要在相关逻辑中检测cancel状态
-
KVO的形式改变NSOperation的属性因为
NSOperation的大部分属性都是只读的,所以过程中需要使用KVO来改变属性值
FileUploadOperation实现
根据文件hash值,以及其它上传接口需要参数初始化
/// 初始化上传做操
/// - Parameters:
/// - fileKey: 文件md5
/// - fileInfo: 文件其它信息
- (instancetype)initWithFileKey:(NSString *)fileKey
fileInfo:(NSDictionary *)fileInfo;
因为同一个文件可能有多场景在上传,对于这种相当于一个文件需要一个NSOperation来处理上传任务,但是要有多个场景的回调。内部定义typedef NSMutableDictionary<NSString *, id> FileCallbacksDictionary;字典结构,key为block回调类型标记(进度&完成),value则为对应的block, 还需要声明一个数组NSMutableArray<FileCallbacksDictionary *> *callbackBlocks 存储这些回调信息
- (id)addHandlersForProgress:(FileUploadProgresBlock)progressBlock
completed:(FileUploadFinishBlock)completedBlock {
FileCallbacksDictionary *callbacks = [NSMutableDictionary new];
// 进度回调
if (progressBlock) {
callbacks[kProgressCallbackKey] = [progressBlock copy];
}
// 完成回调
if (completedBlock) {
callbacks[kCompletedCallbackKey] = [completedBlock copy];
}
// 还会涉及到回调的删除,这里加个锁
@synchronized (self) {
[self.callbackBlocks addObject:callbacks];
}
return callbacks;
}
重写Operation部分方法来实现任务流程和状态控制
start方法,任务开始执行,在这个方法中一般将ready状态的Operation执行任务,进入Executing状态。
- (void)start {
@synchronized (self) {
if (self.isCancelled) {
if (!self.isFinished) {
self.finished = YES;
}
[self callCompletionBlocksWithError:[NSError errorWithDomain:FileUploadErrorDomain
code:FileUploadErrorCancelled
userInfo:@{ NSLocalizedDescriptionKey: @"Operation cancelled by user before sending the request" }]];
[self reset];
return;
}
self.executing = YES;
[self uploadFile];
}
}
cancel方法,取消任务
- (void)cancel {
@synchronized (self) {
[self cancelInternal];
}
}
done 任务完成方法
- (void)done {
self.finished = YES;
self.executing = NO;
[self reset];
}
- (void)reset {
@synchronized (self) {
[self.callbackBlocks removeAllObjects];
}
}
其它方法根据情况重写
- (BOOL)isAsynchronous; //是否异步执行,YES异步,NO不是。
- (void)main; // 对于非并发操作,通常只覆盖这一个方法
- (BOOL)isExecuting; //是否正在执行,YES正在执行。
- (BOOL)isFinished; //是否已经结束,YES结束,从queue中移除。
FileUploadManager实现
单利实现,内部创建对应的NSOperationQueue管理任务,同时需要NSMutableDictionary<NSString *, FileUploadOperation *> *operations; 一个字典来保存operation信息。NSMutableArray<FileUploadToken *> *operationTokenArr; 数组来保存所有的下载任务,不同场景上传相同文件,都会生成对应的FileUploadToken,但是FileUploadOperation只有一个。也可以根据具体的FileUploadToken,来取消对应场景的下载回调。
对外方法,根据需求对外暴漏不同的入参来创建任务队列
/// 上传视频
/// - Parameters:
/// - path: 路径
/// - size: 大小
/// - progress: 进度回调
/// - result: 结果回调
- (void)uploadVideo:(NSString *)path
size:(CGSize)size
progress:(FileUploadProgresBlock)progress
result:(FileUploadFinishBlock)result;
/// 上传图片,path & image & data 三种方式,任选一种
/// - Parameters:
/// - path: 图片路径
/// - image: 图片数据
/// - data: 图片二进制数据
/// - isCompression: 是否压缩
/// - progress: 进度回调
/// - result: 结果回调
- (void)uploadImagePath:(nullable NSString *)path
image:(nullable UIImage *)image
data:(nullable NSData *)data
isCompression:(BOOL)isCompression
progress:(FileUploadProgresBlock)progress
result:(FileUploadFinishBlock)result;
初始化,设置最大并发数量
- (instancetype)init {
self = [super init];
if (!self) {
self.uploadQueue = [[NSOperationQueue alloc] init];
// 根据需求设置最大并发数量
self.uploadQueue.maxConcurrentOperationCount = 5;
self.operations = [NSMutableDictionary new];
self.operationTokenArr = [NSMutableArray new];
}
return self;
}
添加任务,注意判断对应文件的operation是否存在,如果不存在则创建新的operation。如果已经存在,根据对应的回调生成FileUploadToken
- (void)_uploadFileWithFileKey:(NSString *)fileKey
fileInfo:(NSDictionary *)fileInfo
progress:(FileUploadProgresBlock)progress
result:(FileUploadFinishBlock)result
{
WJ_LOCK(_operationsLock);
id uploadOperationCancelToken;
FileUploadOperation *operation = [self.operations objectForKey:fileKey];
BOOL needAdd = NO;
// operation 不存在,或者已经完成和取消,去创建新的operation
if (!operation || operation.isFinished || operation.isCancelled) {
operation = [[FileUploadOperation alloc] initWithFileKey:fileKey
fileInfo:fileInfo];
@weakify(self);
operation.completionBlock = ^{
@strongify(self);
[self removeOperation:fileKey];
};
self.operations[fileKey] = operation;
uploadOperationCancelToken = [operation addHandlersForProgress:progress completed:result];
needAdd = YES;
} else {
// operation 已经存在,添加新的回调
@synchronized (operation) {
uploadOperationCancelToken = [operation addHandlersForProgress:progress completed:result];
}
}
FileUploadToken *token = [[FileUploadToken alloc] initWithUploadOperation:operation];
token.fileKey = fileKey;
token.uploadOperationCancelToken = uploadOperationCancelToken;
token.uploadOperation = operation;
[self.operationTokenArr addObject:token];
if (needAdd) {
[self.uploadQueue addOperation:operation];
}
WJ_UNLOCK(_operationsLock);
}
取消任务
- (void)removeOperation:(NSString *)fileKey {
WJ_LOCK(self->_operationsLock);
[self.operations removeObjectForKey:fileKey];
NSMutableArray *tempTokenArr = [[NSMutableArray alloc] initWithArray:self.operationTokenArr copyItems:YES];
for (FileUploadToken *token in tempTokenArr) {
if ([token.fileKey isEqualToString:fileKey]) {
[self.operationTokenArr removeObject:token];
}
}
WJ_UNLOCK(self->_operationsLock);
}
相对来讲逻辑比较简单,算是记录一下一个小工具类,同时可以扩展批量文件上传等这种多任务场景。