多任务处理-NSOperation
多任务处理,也称作多线程处理。iOS 中主要是 NSOperation 和 GCD。这篇文章总结下 NSOperation 的一些使用方法。
什么是 NSOperation ?
定义
NSOperation 表示的是一个操作。它本身是抽象基类,因此必须使用它的子类,使用NSOperation子类的方式有2种:
-
Foundation框架提供的两个具体子类: NSInvocationOperation和 NSBlockOperation
-
自定义子类继承NSOperation,实现内部相应的方法:
1.重写“main”方法 2.在“main”方法中创建一个“autoreleasepool” 3.将你的代码放在“autoreleasepool”中(ARC中也需要放在autoreleasepool中,这是为了兼容MRC代码) 4.执行操作
开始操作
NSOperation 调用 start 方法即可开始执行操作,NSOperation 对象默认按同步方式执行,也就是在调用start方法的那个线程中直接执行。isConcurrent方法默认返回NO,表示操作与调用线程同步执行。
- (void)createSynchronouslyOperations {
NSNumber *simpleObject = @123;
NSInvocationOperation *operation = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(operationEntry:) object:simpleObject];
[operation start];
}
-(void)operationEntry:(id)paramObject{
NSLog(@"***Parameter Object = %@",paramObject);
NSLog(@"***Main Thread = %@",[NSThread mainThread]);
NSLog(@"***current Thread = %@",[NSThread currentThread]);
}
如果要使NSOperation对象按异步方式执行,需要把它加入到NSOperationQueue,举个例子,我们使用NSInvocationOperation 完成异步的图片下载功能:
- (void)createAsynchronouslyOperations{
NSURL *url = [NSURL URLWithString:@"xxx"];
NSInvocationOperation *operation = [[NSInvocationOperation alloc]initWithTarget:self
selector:@selector(downloadImage:)
object:url];
[operation setCompletionBlock:^() {
NSLog(@"下载完成");
}];
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[queue addOperation:operation];
}
//异步方式下载图片
-(void)downloadImage:(NSString *)url{
UIImage * image = [[UIImage alloc]initWithData:[[NSData alloc]initWithContentsOfURL:[NSURL URLWithString:url]]];
[self performSelectorOnMainThread:@selector(updateUI:) withObject:image waitUntilDone:YES];
}
//下载完成后,切换到主线程更新UI
-(void)updateUI:(UIImage*) image{
self.imageView.image = image;
}
取消操作
operation开始执行之后, 我们可以调用cancel方法中途取消操作:
[operation cancel];
监听操作的执行
每当一个NSOperation执行完毕,它就会调用自身的 completionBlock 属性一次,这然后你就可以在这个 completionBlock 中加入自己的代码逻辑。比如说,你可以在一个网络请求操作的completionBlock来处理操作执行完以后从服务器下载下来的数据。
[operation setCompletionBlock:^() {
NSLog(@"下载完成");
}];
NSBlockOperation
该子类和NSInvocationOperation不同的是,它能够并发地执行一个或多个block对象,所有相关的block都执行完之后,操作才算完成
-(void)BlockOperation{
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^(){
NSLog(@"1,线程:%@", [NSThread currentThread]);
}];
[operation addExecutionBlock:^() {
NSLog(@"2,线程:%@", [NSThread currentThread]);
}];
[operation addExecutionBlock:^() {
NSLog(@"3,线程:%@", [NSThread currentThread]);
}];
[operation addExecutionBlock:^() {
NSLog(@"4,线程:%@", [NSThread currentThread]);
}];
// 开始执行任务
[operation start];
}
打印信息如下
4,线程:<NSThread: 0x7fca4873cea0>{number = 7, name = (null)}
1,线程:<NSThread: 0x7fca4861dbc0>{number = 1, name = main}
2,线程:<NSThread: 0x7fca48652850>{number = 5, name = (null)}
3,线程:<NSThread: 0x7fca48522db0>{number = 6, name = (null)}
number属性可以看成是线程的id,可以看出,这4个block是并发执行的。
自定义NSOperation
如果 NSInvocationOperation 和 NSBlockOperation 对象不能满足普通的需求, 你可以直接继承 NSOperation ,并添加额外的操作。具体方法是通过重写 main 或者 star t方法来定义自己的 operations 。前一种方法非常简单,开发者不需要管理一些状态属性(例如 isExecuting 和 isFinished ),当main方法返回的时候,这个operation就结束了。这种方式使用起来非常简单,但是灵活性相对重写start来说要少一些。
我们新建一个DownloadImageOperation,并使用delegate来完成一个回调。
DownloadImageOperation.h
@protocol DownloadOperationDelegate;
@interface DownloadImageOperation : NSOperation
@property (nonatomic, copy) NSString *imageUrl;
@property (nonatomic, retain) id<DownloadOperationDelegate> delegate;
- (id)initWithUrl:(NSString *)url delegate:(id<DownloadOperationDelegate>)delegate;
@end
@protocol DownloadOperationDelegate <NSObject>
- (void)downloadFinishWithImage:(UIImage *)image;
@end
为了能使用操作队列所提供的取消功能,每一步操作都需要检查 isCancelled 属性。如果为 true,则直接 return 函数。
DownloadImageOperation.m
@implementation DownloadImageOperation
// 初始化
- (id)initWithUrl:(NSString *)url delegate:(id<DownloadOperationDelegate>)delegate {
if (self = [super init]) {
_imageUrl = url;
_delegate = delegate;
}
return self;
}
// 执行主任务
- (void)main {
// 新建一个自动释放池,如果是异步执行操作,那么将无法访问到主线程的自动释放池
@autoreleasepool {
//NSOperation的-cancel状态调用时,会通过KVO通知isCancelled的keyPath来修改isCancelled属性的返回值
if (self.isCancelled) return;
NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:self.imageUrl]];
if (self.isCancelled) {
imageData = nil;
return;
}
// 初始化图片
UIImage *image = [UIImage imageWithData:imageData];
if (self.isCancelled) {
image = nil;
return;
}
if ([self.delegate respondsToSelector:@selector(downloadFinishWithImage:)]) {
// 把图片数据传回到主线程
[(NSObject *)self.delegate performSelectorOnMainThread:@selector(downloadFinishWithImage:) withObject:image waitUntilDone:NO];
}
}
}
@end
我们新建一个控制器来使用这个DownloadImageOperation
@interface DownloadImageOperationController ()<DownloadOperationDelegate>
@property (weak, nonatomic) IBOutlet UIImageView *image;
@end
@implementation DownloadImageOperationController
- (void)viewDidLoad{
[super viewDidLoad];
//加入到 NSOperationQueue 中
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
DownloadImageOperation *customOperation = [[DownloadImageOperation alloc]initWithUrl:kURL delegate:self];
[queue addOperation:customOperation];
}
-(void)downloadFinishWithImage:(UIImage *)image{
self.image.image = image;
}
@end
如果你希望拥有更多的控制权,以及在一个操作中可以执行异步任务,那么就重写 start 方法:
@implementation CustomOperation
- (void)start{
_isExecuting = YES;
_isFinished = NO;
// 开始处理,在结束时应该调用 finished ...
}
- (void)finished{
_isExecuting = NO;
_isFinished = YES;
}
@end
上面讲过,如果要使NSOperation对象按异步方式执行,需要把它加入到NSOperationQueue。NSOperationQueue 有两种不同类型的队列:主队列和自定义队列。为了方便理解,也可以称作主队列和非主队列。主队列运行在主线程之上,而非主队列在后台执行。如上所示,在两种类型中,这些队列所处理的任务都使用NSOperation 的子类来表述。
NSOperationQueue
创建
//后台线程依据
NSOperationQueue *myQueue = [[NSOperationQueue alloc] init];
//主线程
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
添加operation
//添加一个operation
[queue addOperation:operation];
//添加一组operation
[queue addOperations:operations waitUntilFinished:NO];
//将 block 添加到操作队列中。有时候会非常的方便,比如你希望在主队列中调度一个一次性任务:
[queue addOperationWithBlock:^() {
NSLog(@"执行一个新的操作,线程:%@", [NSThread currentThread]);
}];
NSOperation添加到queue之后,绝对不要再修改NSOperation对象的状态。因为NSOperation对象可能会在任何时候运行,因此改变NSOperation对象的依赖或数据会产生不利的影响。你只能查看NSOperation对象的状态, 比如是否正在运行、等待运行、已经完成等
设置队列的最大并发操作数量
通过maxConcurrentOperationCount属性可以控制一个特定队列中可以有多少个操作参与并发执行。将其设置为1的话,你将得到一个串行队列,这在以隔离为目的的时候会很有用。不过operation执行的顺序仍然依赖于其它因素,比如operation是否准备好和operation的优先级等。因此串行化的operation queue并不等同于GCD
[queue setMaxConcurrentOperationCount:1];
队列中operation的优先级
优先级等级则是operation对象本身的一个属性。默认所有operation都拥有“普通”优先级,不过可以通过setQueuePriority:方法来提升或降低operation对象的优先级。这5个优先级包括:
- NSOperationQueuePriorityVeryHigh
- NSOperationQueuePriorityHigh
- NSOperationQueuePriorityNormal
- NSOperationQueuePriorityLow
- NSOperationQueuePriorityVeryLow
优先级只能应用于相同 queue 中的 operations 。如果应用有多个 operation queue, 每个queue的优先级等级是互相独立的。因此不同 queue 中的低优先级操作仍然可能比高优先级操作更早执行。
依赖关系
如果你需要进一步在除了5个标准的优先级以外对operation的执行顺序进行控制的话,还可以在operation之间指定依赖关系,当某个NSOperation对象依赖于其它NSOperation对象的完成时,就可以通过addDependency方法添加一个或者多个依赖的对象,只有所有依赖的对象都已经完成操作,当前NSOperation对象才会开始执行操作。另外,通过removeDependency方法来删除依赖对象。
看下面的例子:
//在某个任务完成后执行特定的任务
-(void)firstOperationEntry:(id)paramObject{
NSLog(@"***Parameter Object = %@",paramObject);
NSLog(@"***Main Thread = %@",[NSThread mainThread]);
NSLog(@"***current Thread = %@",[NSThread currentThread]);
}
-(void)secondOperationEntry:(id)paramObject{
NSLog(@"%s",__FUNCTION__);
NSLog(@"***Parameter Object = %@",paramObject);
NSLog(@"***Main Thread = %@",[NSThread mainThread]);
NSLog(@"***current Thread = %@",[NSThread currentThread]);
}
-(void)DependencyOperation{
NSString *firstNumber = @"1";
NSString *secondNumber = @"2";
NSInvocationOperation *firstOperation = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(firstOperationEntry:) object:firstNumber];
NSInvocationOperation *secondOperation = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(secondOperationEntry:) object:secondNumber];
[firstOperation addDependency:secondOperation];//与之相反的是removeDependency
NSOperationQueue *operationQueue = [[NSOperationQueue alloc]init];
[operationQueue addOperation:firstOperation];
[operationQueue addOperation:secondOperation];
NSLog(@"******Main Thread is here");
}
打印结果
******Main Thread is here
***Parameter Object = 2
***Main Thread = <NSThread: 0x7fd32af02e80>{number = 1, name = (null)}
***current Thread = <NSThread: 0x7fd32aeb5fc0>{number = 5, name = (null)}
***Parameter Object = 1
***Main Thread = <NSThread: 0x7fd32af02e80>{number = 1, name = (null)}
***current Thread = <NSThread: 0x7fd32aeb5fc0>{number = 5, name = (null)}
可以看出,只有secondOperation都已经完成操作,firstOperation才会开始执行操作
依赖关系不局限于相同queue中的NSOperation对象,NSOperation对象会管理自己的依赖, 因此完全可以在不同的queue之间的NSOperation对象创建依赖关系。此外,确保不要创建依赖循环,像A依赖B,B又依赖A。
暂停和继续queue
如果你想临时暂停Operations的执行,可以使用queue的setSuspended:方法暂停queue。不过暂停一个queue不会导致正在执行的operation在任务中途暂停,只是简单地阻止调度新Operation执行。你可以在响应用户请求时,暂停一个queue来暂停等待中的任务。稍后根据用户的请求,可以再次调用setSuspended:方法继续queue中operation的执行
// 暂停queue
[queue setSuspended:YES];
// 继续queue
[queue setSuspended:NO];