NSOperation 实现文件批量上传

413 阅读5分钟

前言

项目中有文件上传的需求,目前是单利实现,仅支持单文件上传,并且没有超时,取消等操作,所以考虑扩展下相关的功能。

文件批量上传方案

  • 手动创建一个任务队列(数组),控制正在执行的任务数量

    开始上传任务时,需要根据生成的文件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);
}

相对来讲逻辑比较简单,算是记录一下一个小工具类,同时可以扩展批量文件上传等这种多任务场景。

完整代码实现在这里