浅析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方法取消未执行的操作。但是已执行或者正在执行的操作不可取消。
即便操作已经被添加到操作队列中也可以取消,只要操作没有开始被执行。

    // 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.)操作的执行优先满足依赖关系,然后再满足优先级。即先根据依赖执行操作,然后再从所有准备好的操作中取出优先级最高的那一个执行。

(五) 队列

(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");

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

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

参考文章

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

转载于:https://my.oschina.net/u/3579120/blog/1533345

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值