浅析iOS多线程编程之NSOperation

前言

和NSThread、GCD一样,NSOperation也是Apple提供的一项多线程并发编程方案。和GCD不同的是,NSOperation是对GCD在OC层面的封装,NSOperation完全面向对象。
默认情况下,NSOperation并不具备封装操作的能力,NSOperation是一个基类,要想封装操作,必须使用它的子类。使用NSOperation子类的方式有3种:

1> NSInvocationOperation(系统提供)
2> NSBlockOperation (系统提供)
3> 自定义子类继承NSOperation,实现内部相应的方法 (自定义)

(一) NSInvocationOperation

NSInvocationOperation初始化的方法有两个,分别如下:

- (nullable instancetype)initWithTarget:(id)target selector:(SEL)sel object:(nullable id)arg;
- (instancetype)initWithInvocation:(NSInvocation *)inv NS_DESIGNATED_INITIALIZER;

(1.1) initWithTarget:(id)target selector:(SEL)sel object:(nullable id)arg

NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(demo) object:nil];
[op start];

解析:上面通过传入一个方法签名(selector)和方法调用者(target)初始化了一个NSInvocationOperation对象。
调用start实例方法可以执行该操作封装的任务。

(1.2) initWithInvocation:(NSInvocation *)inv

NSMethodSignature *sign = [[self class] instanceMethodSignatureForSelector:@selector(demo)];
NSInvocation *inv = [NSInvocation invocationWithMethodSignature:sign];
inv.target = self;
inv.selector = @selector(demo);
NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithInvocation:inv];
[op2 start];

解析:上面通过传入一个NSInvocation对象初始化了一个NSInvocationOperation对象。NSInvocation对象通过传入一个方法签名进行初始化,并且给NSInvocation对象设置了target和selector。

注意:NSInvocationOperation实例对象直接调用start方法是在当前线程执行操作封装的任务。而不是在子线程中执行。也就是说,NSInvocationOperation实例对象直接调用start方法不会开启新线程异步执行,而是同步执行。只有将NSInvocationOperation实例对象添加到一个NSOperationQueue中,才会异步执行操作。

(二) NSBlockOperation

NSBlockOperation是NSOperation的子类。NSBlockOperation中给我们提供了两个方法:

+ (instancetype)blockOperationWithBlock:(void (^)(void))block;
- (void)addExecutionBlock:(void (^)(void))block;

第一个是类方法(静态方法),可以通过类方法直接初始化一个blockOperation对象。
第二个是实例方法(对象方法/动态方法),可以给一个已经存在的NSBlockOperation对象添加额外的任务。

和NSInvocationOperation相比,NSBlockOperation对象不用添加到操作队列也能开启新线程,但是开启新线程是有条件的。前提是一个blockOperation中需要封装多个任务。如下示例,blockOperation中只有一个任务,默认会在当前线程执行。

// 同步执行
    NSBlockOperation *blkop = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"%@", [NSThread currentThread]);    
}];
    [blkop start];

// 输出结果:
NSOperation[1839:133702] <NSThread: 0x608000076b00>{number = 1, name = main}

解析:NSBlockOperation类通过调用类方法blockOperationWithBlock:直接初始化一个NSBlockOperation对象。其中类方法需要一个block作为参数,该block中封装的就是这个NSBlockOperation对象要执行的任务。然后直接调用start实例方法即可触发操作的执行。无需将NSBlockOperation对象加入到操作队列中。

注意:NSBlockOperation对象如果只封装了一个任务, 那么默认会在当前线程中同步执行。

// 异步执行
    NSBlockOperation *blkop = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任务1- %@", [NSThread currentThread]);
    }];
    
    // 添加额外的任务
    [blkop addExecutionBlock:^{
        NSLog(@"任务2- %@", [NSThread currentThread]);
    }];
    [blkop addExecutionBlock:^{
        NSLog(@"任务3- %@", [NSThread currentThread]);
    }];
    [blkop start];
// 输出结果:
2017-02-08 22:41:54.871 NSOperation[1884:142063] 任务1- <NSThread: 0x60800007cec0>{number = 1, name = main}
2017-02-08 22:41:54.871 NSOperation[1884:142100] 任务3- <NSThread: 0x6080002699c0>{number = 4, name = (null)}
2017-02-08 22:41:54.871 NSOperation[1884:142101] 任务2- <NSThread: 0x608000269800>{number = 3, name = (null)}

解析:初始化一个NSBlockOperation对象,然后调用addExecutionBlock:对象方法给这个NSBlockOperation对象添加额外的任务。

注意:一般情况下,如果一个NSBlockOperation对象封装了多个任务。那么除第一个任务外,其他的任务会在新线程(子线程)中执行。即,NSBlockOperation是否开启新线程取决于任务的个数,任务的个数多,会自动开启新线程。但是第一个被执行的任务是同步执行,除第一个任务外,其他任务是异步执行的。

(三) 自定义NSOperation

如果NSInvocationOperation和NSBlockOperation不能满足需求。你可以通过重写 main 或者 start 方法 来定义自己的 operations 。前一种方法非常简单,开发者不需要管理一些状态属性(例如 isExecuting 和 isFinished),当 main 方法返回的时候,这个 operation 就结束了。这种方式使用起来非常简单,但是灵活性相对重写 start 来说要少一些。引自并发编程:API 及挑战。如果只是简单地自定义NSOperation,只需要重载-(void)main这个方法,在这个方法里面添加需要执行的操作。

// EOCOperation.h
#import <Foundation/Foundation.h>

@interface EOCOperation : NSOperation

@end

// EOCOperation.m
#import "EOCOperation.h"

@implementation EOCOperation
- (void)main {
    NSLog(@"%@",[NSThread currentThread]);
}
@end
// 调用自定义operation
EOCOperation *customOperation = [[EOCOperation alloc] init];
[customOperation start];

输出结果:
NSOperation[2084:169435] <NSThread: 0x600000260f80>{number = 1, name = main}

EOCOperation *customOperation = [[EOCOperation alloc] init];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:customOperation];
// 输出结果:
NSOperation[739:22292] <NSThread: 0x600000070280>{number = 3, name = (null)}

自定义operation和NSInvocationOperation一样,如果直接调用start方法,不把operation添加到操作队列中,任务直接在当前线程同步执行。
如果把自定义operation添加到操作队列,那么任务会在新线程中异步执行。

警告:不要即把操作添加到操作队列中,又调用操作的start方法,这样是不允许的!否则运行时直接报错。


NSOperation[756:24507] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSOperationInternal _start:]: something other than the operation queue it is in is trying to start the receiver'

为了能够使用操作队列提供的取消功能,我们需要在main方法中经常性的判断操作有没有被取消,如果操作已经被取消,我们需要立即使main方法返回,不再执行后续代码。在以下情况可能需要判断操作是否已经取消:

  • main方法的开头。因为取消可能发生在任何时候,甚至在operation执行之前。
  • 执行了一段比较耗时的操作后。因为执行耗时操作期间有可能取消了该操作。
  • 其他任何有可能的地方。
    举例来讲:自定义operation的main函数中需要封装网络请求的URL,然后拼接参数。然后发送一个异步请求,请求网络数据。我们需要在以下地方进行判断是否已经取消操作。
- (void)main {
    if (self.isCancelled) {
        return;
    }
    
    // 封装URL
    ......
    if (self.isCancelled) {
        return;
    }
    
    // 拼接参数
    ......
    if (self.isCancelled) {
        return;
    }
    
    // 异步请求
    ......
    if (self.isCancelled) {
        return;
    }
}

如果你希望拥有更多的控制权,以及在一个操作中可以执行异步任务,那么就重写 start 方法:

    - (void)start
    {
        self.isExecuting = YES;
        self.isFinished = NO;
        // 开始处理,在结束时应该调用 finished ...
    }

    - (void)finished
    {
        self.isExecuting = NO;
        self.isFinished = YES;
    }

注意:这种情况下,你必须手动管理操作的状态。 为了让操作队列能够捕获到操作的改变,需要将状态的属性以配合 KVO 的方式进行实现。如果你不使用它们默认的 setter 来进行设置的话,你就需要在合适的时候发送合适的 KVO 消息。

(四) NSOperation其他方法

(4.1) cancel方法

NSOperation除了有start方法,还有cancel方法。我们可以调用cancel方法取消未执行的操作。但是已执行或者正在执行的操作不可取消。
即便操作已经被添加到操作队列中也可以取消,只要操作没有开始被执行。
因为官方文档上是这么说的:This method does not force your operation code to stop. Instead, it updates the object’s internal flags to reflect the change in state. If the operation has already finished executing, this method has no effect. Canceling an operation that is currently in an operation queue, but not yet executing, makes it possible to remove the operation from the queue sooner than usual.

    // 1.封装op1
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        // 封装op2
        NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
            [NSThread sleepForTimeInterval:2];
            NSLog(@"执行op2%@",[NSThread currentThread]);
        }];
        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
        [queue addOperation:op2];
        // 取消op2
        [op2 cancel];
        
        NSLog(@"执行op1- %@", [NSThread currentThread]);
    }];
    [op1 start];

// 输出结果:
2017-02-09 19:46:24.745 NSOperation[1163:67542] 执行op1- <NSThread: 0x6000000770c0>{number = 1, name = main}

解析:上面代码只会执行op1,op2永远也不会执行,因为在op2执行之前就已经通过调用了cancel方法,取消了op2的执行。所以输出结果只有执行op1。且op1是在主线程执行的。
如果我们不取消op2,那么op2也会被执行。只需要注释掉取消op2的代码。

注意:我们可以通过调用cancel方法取消某个尚未执行的操作(无论这个操作是否被加入了操作队列)。但是我们不能取消正在执行或者已经执行完的操作。

(4.2) completionBlock属性

NSOperation提供了一个block类型的completionBlock属性。如果想在操作执行完毕之后,还希望做一些其他的事情,可以通过completionBlock实现。

无论操作是直接调用start执行还是加入到操作队列中执行,也无论操作是同步执行还是异步执行。completionBlock永远是等待操作所有任务执行完毕最后被调用。

同步执行

    NSBlockOperation *blkop = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"执行%@",[NSThread currentThread]);
    }];
    blkop.completionBlock = ^{
        NSLog(@"完成");
    };
    [blkop start];

// 输出结果:
2017-02-09 20:03:30.387 NSOperation[1395:94883] 执行<NSThread: 0x600000065d40>{number = 1, name = main}
2017-02-09 20:03:30.388 NSOperation[1395:94930] 完成

异步执行

NSBlockOperation *blkop = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"执行%@",[NSThread currentThread]);
    }];
    blkop.completionBlock = ^{
        NSLog(@"完成");
    };
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:blkop];

// 输出结果:
2017-02-09 20:00:05.145 NSOperation[1364:91326] 执行<NSThread: 0x60800007d500>{number = 3, name = (null)}
2017-02-09 20:00:05.146 NSOperation[1364:91329] 完成

给操作设置completionBlock,必须要在操作被执行前添加,也就是在操作start之前添加,以下的做法是错误的:

NSBlockOperation *blkop = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"执行%@",[NSThread currentThread]);
    }];
[blkop start];
blkop.completionBlock = ^{
        NSLog(@"完成");
    };

如果一个操作是被加到操作队列中,然后才设置completionBlock,这样是可以的,如下:

    NSBlockOperation *blkop = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"执行%@",[NSThread currentThread]);
    }];
    
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:blkop];
    blkop.completionBlock = ^{
        NSLog(@"完成");
    };

总结:如果操作是通过调用start方法触发的,那么completionBlock必须要在start之前设置。如果操作是通过加入操作队列被触发的,那么completionBlock可以在操作添加到操作队列之后设置,只要保证此时操作没有被执行即可。

(4.3) 给NSOperation添加Dependency

默认操作的执行是无序的,NSOperation之间可以通过设置依赖来保证操作执行的顺序。
比如一定要让操作A执行完后,才能执行操作B,可以这么写:
[operationB addDependency:operationA];
操作B依赖于操作A,所以一定要等操作A执行完毕才能执行操作B。
操作的执行顺序不是取决于谁先被添加到队列中,而是取决于操作依赖。也就是说,添加顺序不会决定执行顺序,只有依赖才会决定执行顺序(maxConcurrentOperationCount = 1除外,因为maxConcurrentOperationCount = 1时,操作队列为串行队列,如果没有给操作添加依赖,此时操作的执行顺序取决于操作添加到队列中的先后顺序。即便如此,maxConcurrentOperationCount = 1时,队列中的操作也并不一定在同一个线程中执行)。

即操作依赖可以控制操作的执行顺序,使多个并行的操作可以按照串行的顺序一个一个地执行。如果没有给操作添加依赖,设置操作队列的maxConcurrentOperationCount = 1也可以控制操作的执行顺序,其执行顺序取决于操作添加到队列中的顺序。
如果操作设置了依赖,也给队列设置了maxConcurrentOperationCount = 1。那么操作被执行的顺序取决于依赖。即,依赖的优先级较高。

    NSBlockOperation *blkop1 = [NSBlockOperation blockOperationWithBlock:^{

        NSLog(@"执行1 %@",[NSThread currentThread]);
    }];
    
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    queue.maxConcurrentOperationCount = 1;

    NSBlockOperation *blkop2 = [NSBlockOperation blockOperationWithBlock:^{

        NSLog(@"执行2 %@",[NSThread currentThread]);
    }];
    
    [blkop1 addDependency:blkop2];
    [queue addOperation:blkop1];
    [queue addOperation:blkop2];
    
// 输出结果:
2017-02-09 20:57:13.369 NSOperation[2371:194728] 执行2 <NSThread: 0x600000267700>{number = 3, name = (null)}
2017-02-09 20:57:13.375 NSOperation[2371:194725] 执行1 <NSThread: 0x6000002615c0>{number = 4, name = (null)}

解析:虽然设置了队列的maxConcurrentOperationCount = 1,使操作队列变成一个串行队列。但是也设置了操作之间的依赖,所以最终操作的执行顺序取决于依赖。所以上面的执行结果永远是先执行操作2,再执行操作1。

注意:一定要在操作添加到队列之前设置操作之间的依赖,否则操作已经添加到队列中在设置依赖,依赖不会生效。

问题:默认情况下,操作队列中的操作的执行顺序真的是无序的吗?
个人认为,默认情况下,操作队列中的操作执行顺序就是其被取出的顺序,也是其被添加到队列中的顺序,操作的执行顺序是有序的,但是操作执行完成的顺序是无需的。也就是说,因为不同的操作执行完成所需要的时间不同,最先从对垒中取出执行的操作不一定先执行完成,后执行的操作不一定后执行完成。所以,给人的感觉就是操作的执行是无序的。

其实,操作的依赖特性可以用GCD的信号量机制来实现。

不同队列的操作之间也可以设置依赖

依赖关系不局限于相同queue中的NSOperation对象,NSOperation对象会管理自己的依赖, 因此不同的操作队列之间的操作也可以设置依赖。如下:

    NSBlockOperation *blkop1 = [NSBlockOperation blockOperationWithBlock:^{

        NSLog(@"执行1 %@",[NSThread currentThread]);
    }];
    
    NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
    queue1.maxConcurrentOperationCount = 1;

    NSBlockOperation *blkop2 = [NSBlockOperation blockOperationWithBlock:^{

        NSLog(@"执行2 %@",[NSThread currentThread]);
    }];
    
    [blkop2 addDependency:blkop1];
    [queue1 addOperation:blkop1];
    [queue1 addOperation:blkop2];
    
    NSBlockOperation *blkop3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"执行3 %@",[NSThread currentThread]);
    }];
    [blkop3 addDependency:blkop2];
    
    NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
    queue2.maxConcurrentOperationCount = 1;
    
    [queue2 addOperation:blkop3];
    
// 输出结果:
2017-02-09 21:20:52.270 NSOperation[2545:217909] 执行1 <NSThread: 0x60000007a480>{number = 3, name = (null)}
2017-02-09 21:20:52.272 NSOperation[2545:217909] 执行2 <NSThread: 0x60000007a480>{number = 3, name = (null)}
2017-02-09 21:20:52.273 NSOperation[2545:217907] 执行3 <NSThread: 0x60000007a080>{number = 4, name = (null)}

解析:上面代码中,不仅队列queue1中的两个操作blkop1和blkop2间设置了依赖。两个不同的操作队列queue1和queue2之间的操作blkop2和blkop3也设置了依赖。最中依赖顺序是:blkop2 依赖 blkop1,blkop3依赖blkop2。所以操作的执行顺序永远是1、2、3。

NSOperation提供了如下三个接口管理自己的依赖:

- (void)addDependency:(NSOperation *)op;
- (void)removeDependency:(NSOperation *)op;

@property (readonly, copy) NSArray<NSOperation *> *dependencies;

警告:操作间不能循环依赖,比如A依赖B,B依赖A,这是错误的。

(4.4) queuePriority

NSOperation类提供了一个queuePriority属性,代表操作在队列中执行的优先级

@property NSOperationQueuePriority queuePriority;

queuePriority是一个NSOperationQueuePriority类型的枚举值,apple为NSOperationQueuePriority类型定义了一下几个值:

typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};

queuePriority默认值是NSOperationQueuePriorityNormal。根据实际需要我们可以通过调用queuePriority的setter方法修改某个操作的优先级。

    NSBlockOperation *blkop1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"执行blkop1");
    }];
    
    NSBlockOperation *blkop2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"执行blkop2");
    }];
    
    // 设置操作优先级
    blkop1.queuePriority = NSOperationQueuePriorityLow;
    blkop2.queuePriority = NSOperationQueuePriorityVeryHigh;
 
    NSLog(@"blkop1 == %@",blkop1);
    NSLog(@"blkop2 == %@",blkop2);

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    // 操作添加到队列
    [queue addOperation:blkop1];
    [queue addOperation:blkop2];
    
    NSLog(@"%@",[queue operations]);
    for (NSOperation *op in [queue operations]) {
        NSLog(@"op == %@",op);
    }

// 输出结果:
2017-02-12 19:36:01.149 NSOperation[1712:177976] blkop1 == <NSBlockOperation: 0x608000044440>
2017-02-12 19:36:01.150 NSOperation[1712:177976] blkop2 == <NSBlockOperation: 0x6080000444d0>
2017-02-12 19:36:01.150 NSOperation[1712:177976] (
    "<NSBlockOperation: 0x608000044440>",
    "<NSBlockOperation: 0x6080000444d0>"
)
2017-02-12 19:36:01.150 NSOperation[1712:177976] op == <NSBlockOperation: 0x608000044440>
2017-02-12 19:36:01.150 NSOperation[1712:177976] op == <NSBlockOperation: 0x6080000444d0>
2017-02-12 19:36:01.150 NSOperation[1712:178020] 执行blkop1
2017-02-12 19:36:01.151 NSOperation[1712:178021] 执行blkop2

解析:
(1.)上面创建了两个blockOperation并且分别设置了优先级。显然blkop1的优先级低于blkop2的优先级。然后调用了队列的addOperation:方法使操作入队。最后输出结果证明,操作在对列中的顺去取决于addOperation:方法而不是优先级。
(2.)虽然blkop2优先级高于blkop1,但是bloop1却先于blkop2执行完成。所以,优先级高的操作不一定先执行完成。

注意:
(1.)优先级只能应用于相同queue中的operations。
(2.)操作的优先级高低不等于操作在队列中排列的顺序。换句话说,优先级高的操作不代表一定排在队列的前面。后入队的操作有可能因为优先级高而先被执行。PS:操作在队列中的顺序取决于队列的addOperation:方法。(证明代码如下)
(3.)优先级高只代表先被执行。不代表操作先被执行完成。执行完成的早晚还取决于操作耗时长短。
(4.)优先级不能替代依赖,优先级也绝不等于依赖。优先级只是对已经准备好的操作确定其执行顺序。
(5.)操作的执行优先满足依赖关系,然后再满足优先级。即先根据依赖执行操作,然后再从所有准备好的操作中取出优先级最高的那一个执行。

(4.5) qualityOfService

@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0);

qualityOfService是NSOperation类提供的一个属性。qualityOfService即服务质量

(五) 队列

(5.1) 取消

一旦操作添加到operation queue中,queue就拥有了这个Operation对象并且不能被删除,唯一能做的事情是取消。你可以调用Operation对象的cancel方法取消单个操作,也可以调用operation queue的cancelAllOperations方法取消当前queue中的所有操作。

- (void)cancelAllOperations;

队列通过调用对象方法- (void)cancelAllOperations;可以取消队列中尚未执行的操作。但是正在执行的操作不能够取消。

(5.2) 暂停、恢复

@property (getter=isSuspended) BOOL suspended;

队列中的操作也可以暂停、恢复。通过调用suspended的set方法控制暂停还是恢复。如果传入YES,代表暂停,传入NO代表恢复。

(5.3) 主队列

@property (class, readonly, strong) NSOperationQueue *mainQueue NS_AVAILABLE(10_6, 4_0);

操作队列给我们提供了获取主队列的属性mainQueue。如果想让某些操作在主线程执行,可以直接把操作添加到mainQueue中。

(5.4) maxConcurrentOperationCount

maxConcurrentOperationCount代表队列同一时间允许执行的最多的任务数。或者理解为同一时间允许执行的最多线程数。
maxConcurrentOperationCount默认为-1,代表不限制。
maxConcurrentOperationCount 必须要提前设置,如果队列中添加了操作再设置maxConcurrentOperationCount就无效了。
警告:如果希望操作在主线程中执行,不要设置maxConcurrentOperationCount = 0。直接把操作添加到mainQueue中即可。

(5.5) waitUntilAllOperationsAreFinished

为了最佳的性能,你应该设计你的应用尽可能地异步操作,让应用在Operation正在执行时可以去处理其它事情。如果需要在当前线程中处理operation完成后的结果,可以使用NSOperation的waitUntilFinished方法阻塞当前线程,等待operation完成。通常我们应该避免编写这样的代码,阻塞当前线程可能是一种简便的解决方案,但是它引入了更多的串行代码,限制了整个应用的并发性,同时也降低了用户体验。绝对不要在应用主线程中等待一个Operation,只能在第二或次要线程中等待。阻塞主线程将导致应用无法响应用户事件,应用也将表现为无响应。

    // 会阻塞当前线程,等到某个operation执行完毕  
    [operation waitUntilFinished];  

除了等待单个Operation完成,你也可以同时等待一个queue中的所有操作,使用NSOperationQueue的waitUntilAllOperationsAreFinished方法。注意:在等待一个 queue时,应用的其它线程仍然可以往queue中添加Operation,因此可能会加长线程的等待时间。

    // 阻塞当前线程,等待queue的所有操作执行完毕  
    [queue waitUntilAllOperationsAreFinished];  

注意:waitUntilAllOperationsAreFinished一定要在操作队列添加了操作后再设置。即,先向operation queue中添加operation,再调用[operationQueue waitUntilAllOperationsAreFinished]

    NSBlockOperation *blkop = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"执行操作 %@",[NSThread currentThread]);
    }];
    
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    [queue addOperation:blkop];
    // waitUntilAllOperationsAreFinished就像GCD的barrier一样起到隔离作用
    // waitUntilAllOperationsAreFinished必须要在操作添加到队列后设置
    // waitUntilAllOperationsAreFinished必须要在NSLog(@"finish");之前设置
waitUntilAllOperationsAreFinished
    [queue waitUntilAllOperationsAreFinished];
    
    NSLog(@"finish");

(5.6)operationCount

operationCount,顾名思义,就是指在队列中当前操作而数量。因为只有队列中的操作被执行完成后,这个operationCount值才会改变,所以operationCount值包括当前正在执行的operation和还没有被执行的操、operation。我们获取到的operationCount只是当前队列里操作数量的瞬间值。当我们用到这个operationCount的时候,很有可能队列中实际的operationCount已经发生了改变(因为操作有可能是异步执行的)。所以,苹果不建议我们在开发中使用这个值(比如根据这个值遍历数组中的操作或者进行一些其他精确的计算),很有可能引起错误甚至程序崩溃(比如,数组越界)。

如果你非要使用的话,建议你使用KVO的方式监听队列的operationCount的变化。杜绝直接使用operationCount。好吧,这句话是苹果爸爸说的:You may monitor changes to the value of this property using Key-value observing. Configure an observer to monitor the operationCount key path of the operation queue.

例如:工作中曾经写过的一段代码,就有可能引起崩溃:
(自定义了一个NSOperationQueue,并且覆写了addOperation:方法,当初写这段代码的初衷是当操作很多的时候,舍弃多余的操作,只处理maxTaskCount个操作)

-(void)addOperation:(NSOperation *)op{
    self.suspended = YES;
    
    NSInteger cancelCount = self.operationCount - self.maxTaskCount;
    if (self.operationCount > self.maxTaskCount) {
        for (int i = 0; i < cancelCount; i++) {
            NSOperation *op = [self.operations objectAtIndex:i];
            if([op isKindOfClass:[NSOperation class]]){
                [op cancel];
            }else{
                break;
            }
        }
    }
    
    self.suspended = NO;
    [super addOperation:op];
}

文/VV木公子(简书作者)
PS:如非特别说明,所有文章均为原创作品,著作权归作者所有,转载请联系作者获得授权,并注明出处,所有打赏均归本人所有!

如果您是iOS开发者,或者对本篇文章感兴趣,请关注本人,后续会更新更多相关文章!敬请期待!

参考文章

多线程编程2-NSOperation
多线程编程3 - NSOperationQueue
并发编程:API 及挑战

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

推荐阅读更多精彩内容