前言:
前阵子遇到七牛文件批量上传的问题,尝试了几种方案,现分享一种目前采用的方案——自定义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创建上传任务的问题在于创建多少执行多少,网络资源被竞争而导致失败。
解决这个问题的关键就是控制上传任务同时执行的个数,保证网络充分利用,又没有过多的竞争。
可选方案:
-
手工创建一个上传任务待执行池(数组),控制执行池的任务数量
创建文件上传任务的同时,将需要上传的文件(路径)放入一个临时数组,文件上传结束后,从数组中移除这个文件(路径)。
设置同时上传任务最大数量,每次创建新任务之前检查数组中的文件数,没有达到最大数量时允许创建,反之不允许创建。
-
自定义NSOperation,将operation加入queue做并发控制
将每个上传任务封装成一个NSOperation,直接加入NSOperationQueue实例,同时设置最大并发数。
-
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。
- 几个要点
- 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-onlyisAsynchronous
- read-onlyisExecuting
- read-onlyisFinished
- read-onlyisReady
- read-onlydependencies
- read-onlyqueuePriority
- readable and writablecompletionBlock
- 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