七牛文件批量上传之自定义NSOperation

前言:

前阵子遇到七牛文件批量上传的问题,尝试了几种方案,现分享一种目前采用的方案——自定义operation。

为什么要自己实现七牛文件的批量上传

在使用七牛云存储服务的过程中,想要在手机客户端进行图片、视频等文件的上传,只需要直接引入SDK,然后使用QiniuSDK即可。对于iOS端的上传特别简单,只需要使用pod引入SDK:

pod "Qiniu", "~> 7.0"

然后

#import <QiniuSDK.h>
...
NSString *token = @"从服务端SDK获取";
QNUploadManager *upManager = [[QNUploadManager alloc] init];
NSData *data = [@"Hello, World!" dataUsingEncoding : NSUTF8StringEncoding];
[upManager putData:data key:@"hello" token:token
complete: ^(QNResponseInfo *info, NSString *key, NSDictionary *resp) {
NSLog(@"%@", info);
NSLog(@"%@", resp);
} option:nil];
...

十分简单的操作就能实现。

然而这种使用方法仅适用于单个或少量的文件上传,一旦文件数量较多,就会出现网络资源竞争导致超时或者其他问题,使文件上传失败。

这是由于这种方式会直接创建一个上传任务,同时开启的任务会同时执行。

如何实现七牛文件批量上传

直接使用七牛SDK创建上传任务的问题在于创建多少执行多少,网络资源被竞争而导致失败。

解决这个问题的关键就是控制上传任务同时执行的个数,保证网络充分利用,又没有过多的竞争。

可选方案:

  1. 手工创建一个上传任务待执行池(数组),控制执行池的任务数量

    创建文件上传任务的同时,将需要上传的文件(路径)放入一个临时数组,文件上传结束后,从数组中移除这个文件(路径)。

    设置同时上传任务最大数量,每次创建新任务之前检查数组中的文件数,没有达到最大数量时允许创建,反之不允许创建。

  1. 自定义NSOperation,将operation加入queue做并发控制

    将每个上传任务封装成一个NSOperation,直接加入NSOperationQueue实例,同时设置最大并发数。

  2. GCD中的dispatch_semaphore_t来控制任务执行数量。

    设置信号量阈值为同时执行的上传任务最大数量,每创建一个任务前wait信号,每完成一个发送signal。

最优方案:

以上三种方案,都能解决批量文件上传与网络资源竞争的问题。

不过方案1需要控制数组的多线程访问(加锁),方案3也需要维护一个全局的dispatch_semaphore_t实例,同时方案1和3都不支持任务取消

方案2直接使用NSOperationQueue可以很方便的取消所有任务。处理一些用户的退出登录等动作就很容易。

方案2最优。

自定义NSOperation

  • 基本概念

NSOperation用于iOS的多线程编程中,我们想要执行一个异步任务,可以将这个任务封装成一个NSOperation,创建这个NSOperation实例,并启动就可以达到异步执行的目的。

NSOperation是一个抽象类,不能直接实例化。

我们想使用NSOperation来执行具体任务,就必须创建NSOperation的子类或者使用系统预定义的两个子类,NSInvocationOperation和 NSBlockOperation。

  • 几个要点
  1. NSOperation有几个执行状态:
    官方文档
Key Path Description
isReady The isReady key path lets clients know when an operation is ready to execute. The ready property contains the value YES when the operation is ready to execute now or NO if there are still unfinished operations on which it is dependent./br In most cases, you do not have to manage the state of this key path yourself. If the readiness of your operations is determined by factors other than dependent operations, however—such as by some external condition in your program—you can provide your own implementation of the ready property and track your operation’s readiness yourself. It is often simpler though just to create operation objects only when your external state allows it./brIn OS X v10.6 and later, if you cancel an operation while it is waiting on the completion of one or more dependent operations, those dependencies are thereafter ignored and the value of this property is updated to reflect that it is now ready to run. This behavior gives an operation queue the chance to flush cancelled operations out of its queue more quickly.
isExecuting The isExecuting key path lets clients know whether the operation is actively working on its assigned task. The executing property must report the value YES if the operation is working on its task or NO if it is not./br If you replace the start method of your operation object, you must also replace the executing property and generate KVO notifications when the execution state of your operation changes.
isFinished The isFinished key path lets clients know that an operation finished its task successfully or was cancelled and is exiting. An operation object does not clear a dependency until the value at the isFinished key path changes to YES. Similarly, an operation queue does not dequeue an operation until the finished property contains the value YES. Thus, marking operations as finished is critical to keeping queues from backing up with in-progress or cancelled operations./brIf you replace the start method or your operation object, you must also replace the finished property and generate KVO notifications when the operation finishes executing or is cancelled.
isCancelled The isCancelled key path lets clients know that the cancellation of an operation was requested. Support for cancellation is voluntary but encouraged and your own code should not have to send KVO notifications for this key path. The handling of cancellation notices in an operation is described in more detail in Responding to the Cancel Command.

NSOperation在创建后进入isReady状态方可开始需要执行的任务;
任务执行中进入isExecuting状态;
执行结束后进入isFinished状态,同时如果该NSOperation是在NSOperationQueue中,会从queue中移除;
任务未开始执行前,可以取消NSOperation任务,取消后进入isCancelled状态。

需要注意的是,已经cancel掉的NSOperation是不能再执行的,所以你需要在实现自定义NSOperation时经常检查该NSOperation是否被cancel了。

  • KVO的形式改变NSOperation的属性

    KVO-Compliant Properties
    The NSOperation class is key-value coding (KVC) and key-value observing (KVO) compliant for several of its properties. As needed, you can observe these properties to control other parts of your application. To observe the properties, use the following key paths:

    isCancelled - read-only

    isAsynchronous - read-only

    isExecuting - read-only

    isFinished - read-only

    isReady - read-only

    dependencies - read-only

    queuePriority - readable and writable

    completionBlock - readable and writable

  • 子类需要覆盖的方法

    For non-concurrent operations, you typically override only one method:

    • main

    If you are creating a concurrent operation, you need to override the following methods and properties at a minimum:

    • start

      任务开始执行,在这个方法中一般将ready状态的Operation执行任务,进入Executing状态。

    • asynchronous

      是否异步执行,YES异步,NO不是。

    • executing

      是否正在执行,YES正在执行。

    • finished
      是否已经结束,YES结束,从queue中移除。

示例

自定义NSOperation代码。

.h文件

#import <UIKit/UIKit.h>
@class QNUploadManager;

@interface WTOperation : NSOperation

@property (nonatomic, copy) NSString *wt_identifier;


+ (instancetype)operationWithUploadManager:(QNUploadManager *)uploadManager
                                  filePath:(NSString *)filePath
                                  key:(NSString *)key
                                token:(NSString *)token
                              success:(void(^)())success
                              failure:(void(^)(NSError *error))failure;
- (instancetype)initWithUploadManager:(QNUploadManager *)uploadManager
                             filePath:(NSString *)filePath
                             key:(NSString *)key
                           token:(NSString *)token
                         success:(void (^)())success
                         failure:(void (^)(NSError *))failure;

@end

.m文件

#import "WTOperation.h"
#import <QiniuSDK.h>

static NSString * const WTOperationLockName = @"WTOperationLockName";

@interface WTOperation ()

@property (nonatomic, strong) QNUploadManager *uploadManager;
@property (nonatomic, copy) NSString *filePath;
@property (nonatomic, copy) NSString *key;
@property (nonatomic, copy) NSString *token;
@property (nonatomic, copy) void (^success)();
@property (nonatomic, copy) void (^failure)(NSError *error);

@property (nonatomic, strong) NSRecursiveLock *lock;
@property (nonatomic, copy) NSArray *runLoopModes;


@end

@implementation WTOperation

@synthesize executing = _executing;
@synthesize finished  = _finished;
@synthesize cancelled = _cancelled;

#pragma mark - init
+ (instancetype)operationWithUploadManager:(QNUploadManager *)uploadManager filePath:(NSString *)filePath key:(NSString *)key token:(NSString *)token success:(void (^)())success failure:(void (^)(NSError *))failure {
    WTOperation *operation = [[self alloc] initWithUploadManager:uploadManager filePath:filePath key:key token:token success:success failure:failure];
    return operation;
}

- (instancetype)initWithUploadManager:(QNUploadManager *)uploadManager filePath:(NSString *)filePath key:(NSString *)key token:(NSString *)token success:(void (^)())success failure:(void (^)(NSError *))failure {
    if (self = [super init]) {
        self.uploadManager = uploadManager;
        self.filePath = filePath;
        self.key = key;
        self.token = token;
        self.success = success;
        self.failure = failure;
        
        self.lock = [[NSRecursiveLock alloc] init];
        self.lock.name = WTOperationLockName;
        self.runLoopModes = @[NSRunLoopCommonModes];
    }
    return self;
}

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"WTAsyncOperation"];
        
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

+ (NSThread *)operationThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    
    return _networkRequestThread;
}

#pragma mark - operation
- (void)cancel {
    [self.lock lock];
    if (!self.isCancelled && !self.isFinished) {
        [super cancel];
        [self KVONotificationWithNotiKey:@"isCancelled" state:&_cancelled stateValue:YES];
        if (self.isExecuting) {
            [self runSelector:@selector(cancelUpload)];
        }
    }
    [self.lock unlock];
}

- (void)cancelUpload {
    self.success = nil;
    self.failure = nil;
}

- (void)start {
    [self.lock lock];
    if (self.isCancelled) {
        [self finish];
        [self.lock unlock];
        return;
    }
    if (self.isFinished || self.isExecuting) {
        [self.lock unlock];
        return;
    }
    [self runSelector:@selector(startUpload)];
    [self.lock unlock];
}

- (void)startUpload {
    if (self.isCancelled || self.isFinished || self.isExecuting) {
        return;
    }
    [self KVONotificationWithNotiKey:@"isExecuting" state:&_executing stateValue:YES];
    [self uploadFile];
}

- (void)uploadFile {
    __weak typeof(self) weakSelf = self;
    [self.uploadManager putFile:self.filePath key:self.key token:self.token complete:^(QNResponseInfo *info, NSString *key, NSDictionary *resp) {
        if (weakSelf) {
            if (resp == nil) {
                if (weakSelf.failure) {
                    weakSelf.failure(info.error);
                }
            } else {
                if (weakSelf.success) {
                    weakSelf.success();
                }
            }
            [weakSelf finish];
        }
    } option:nil];
}

- (void)finish {
    [self.lock lock];
    if (self.isExecuting) {
        [self KVONotificationWithNotiKey:@"isExecuting" state:&_executing stateValue:NO];
    }
    [self KVONotificationWithNotiKey:@"isFinished" state:&_finished stateValue:YES];
    [self.lock unlock];
}

- (BOOL)isAsynchronous {
    return YES;
}

- (void)KVONotificationWithNotiKey:(NSString *)key state:(BOOL *)state stateValue:(BOOL)stateValue {
    [self.lock lock];
    [self willChangeValueForKey:key];
    *state = stateValue;
    [self didChangeValueForKey:key];
    [self.lock unlock];
}

- (void)runSelector:(SEL)selecotr {
    [self performSelector:selecotr onThread:[[self class] operationThread] withObject:nil waitUntilDone:NO modes:self.runLoopModes];
}

@end

完整代码在这里

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容