iOS多线程 -- NSOperation相关学习笔记

NSOperation 与 NSOperationQueue

NSOperation和NSOperationQueue是Apple基于GCD封装的一套面向对象的API.

使用Operation的优势如下:

  1. 可以给代码块添加completionBlock, 在任务完成以后自己调用. 相对于GCD代码更简洁.(类似于GCD的dispatch_block_wait/dispatch_block_notify)
  2. operation之间可以添加依赖关系. (addDependency)
  3. 设置operation的优先级.(类似与gcd block的qos)
  4. 方便的设置operation取消操作(gcd的dispatch_block_cancel)
  5. 使用KVO观察对operation状态的监听: isExcuting, isFinished, isCancelled.

NSOperation就是操作, 类似GCD中的block.通常有NSInvocationOperation、NSBlockOperation, 以及自定义NSOperation三种.

NSOperationQueue 是操作队列, 即存放operation的队列. NSOperationQueue将Operation添加到队列中以后, Operation首先进入ready状态(是否ready取决与不同operation之间的依赖dependency), 如果ready状态的operation会开始按照operation的优先级, 顺序被调用执行.操作队列通过maxConcurrentOperationCount属性, 控制并发,串行.(类似于GCD的并行和串行队列).

NSOperation的生命周期

生命周期

如上图所示具有四个阶段:

  • Pending
  • Ready
  • Executing
  • Finished

当我们初始化创建一个NSOperation实例的时候, NSOperation的状态是Pending状态.

如果该operation没有依赖, 被添加到NSOperationQueue的时, 状态会依次 Ready -> Executing -> Finished.

如果有依赖, 被添加到NSOperationQueue的时, 状态会是Pending. 等待前序Operation执行, 状态成为Finished以后, 本Operation状态才会转化成Ready, 然后 Ready -> Executing -> Finished.

cancelled状态

除此之外, 在Pending, Ready, Executing状态都可以调用cancel, 让Operation进入 cancelled状态.

注意, cancelled 状态和 finished状态有race condition.由于cancel只会将cancelled属性设置为YES, 在实际情况中, 有些情况是无法立刻取消的. 例如有一个取消按钮, 点击以后取消需要一定的时间,在这段时间内容, Operation结果完成Finished, 如何处理这种情况? 因此Finished状态与isCancelled状态的的临界区域需要我们去用多线程方法去保护, 避免多线程竞争导致意想不到的结果.

NSOperation多种类型

NSOperation的种类有三种:

  • NSInvocationOperation
  • NSBlockOperation
  • 自定义NSOperation

NSOperation有三种状态,isReady, isExecuting, isFinished.还有很多其他属性, 随着执行操作变化而变化.

NSInvocationOperation调用start执行

调用实例:

/**
 1. start---<NSThread: 0x604000071440>{number = 1, name = main}
 2. 1---<NSThread: 0x604000071440>{number = 1, name = main}
 3. 2---<NSThread: 0x604000071440>{number = 1, name = main}
 4. end--- <NSThread: 0x604000071440>{number = 1, name = main}
 */
- (void) operationStartDemo2 {
    NSLog(@"start---%@", [NSThread currentThread]);

    // 1.创建 NSInvocationOperation 对象
    NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task2) object:nil];

    // 2.调用 start 方法开始执行操作
    [op start];
    NSLog(@"end--- %@", [NSThread currentThread]);
}

- (void)task2 {
    for (int i = 0; i < 2; i++) {
        [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
        NSLog(@"%d---%@", i,[NSThread currentThread]); // 打印当前线程
    }
}

结果解释:

这种创建Operation(NSBlockOperation/NSInvocationOperation), 然后直接调用start(), 会在当前线程中执行task.(类似于创建一个block,然后直接调用block()).

NSBlockOperation

/**
 1. start---<NSThread: 0x600000070d00>{number = 1, name = main}
 2. 0---<NSThread: 0x600000070d00>{number = 1, name = main}
 3. 1---<NSThread: 0x600000070d00>{number = 1, name = main}
 4. end--- <NSThread: 0x600000070d00>{number = 1, name = main}
 */
- (void)operationStartDemo3 {
    NSLog(@"start---%@", [NSThread currentThread]);
    // 1.创建blockOperation
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"%d---%@", i,[NSThread currentThread]); // 打印当前线程
        }
    }];
    // 2.调用 start 方法开始执行操作
    [op start];
    NSLog(@"end--- %@", [NSThread currentThread]);
}

如果NSBlockOperation中只有一个任务, 那么调用start在当前线程中同步执行.

/**
 1. start---<NSThread: 0x60400006c480>{number = 1, name = main}
 2. 4---<NSThread: 0x60400027b500>{number = 4, name = (null)}
 3. 2---<NSThread: 0x60400006c480>{number = 1, name = main}
 4. 1---<NSThread: 0x60400027b5c0>{number = 5, name = (null)}
 5. 3---<NSThread: 0x60000026d280>{number = 3, name = (null)}
 6. 1---<NSThread: 0x60400027b5c0>{number = 5, name = (null)}
 7. 2---<NSThread: 0x60400006c480>{number = 1, name = main}
 8. 4---<NSThread: 0x60400027b500>{number = 4, name = (null)}
 9. 3---<NSThread: 0x60000026d280>{number = 3, name = (null)}
 10 end--- <NSThread: 0x60400006c480>{number = 1, name = main}
 */
- (void)blockOperationAddOperationDemo {
    NSLog(@"start---%@", [NSThread currentThread]);
    // 1.创建blockOperation
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
        }
    }];

    // 2.添加额外的操作
    [op addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
        }
    }];
    [op addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
        }
    }];
    [op addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
        }
    }];

    // 2.调用 start 方法开始执行操作
    [op start];
    NSLog(@"end--- %@", [NSThread currentThread]);
}

在NSBlockOperation对象上,调用addExecutionBlock添加了多个task, 结果看出, operation都是在子线程中运行的, 系统会开启多个子线程去并行运行加入的blocks.

再看下addExecutionBlock:的注释:

Adds the specified block to the receiver’s list of blocks to perform.
The specified block should not make any assumptions about its execution environment.
Calling this method while the receiver is executing or has already finished causes an NSInvalidArgumentException exception to be thrown.

如果NSBlockOperation的状态是excuting,finished, 调用该方法会抛出异常.

总结:

一般情况下,如果一个 NSBlockOperation 对象封装了多个操作。NSBlockOperation 是否开启新线程,取决于操作的个数。如果添加的操作的个数多,就会自动开启新线程。当然开启的线程数是由系统来决定的。

NSOperation是一锤子买卖, 也就是说NSOperation在状态变化以后, 不能返回到原来的状态.

自定义NSOperation

调用方法同前面的一样, 直接调用start方法或者加入到operationQueue中.

NSOperation的结束监听

NSOperation可以设置一个 comletionBlock, 在NSOperation执行完成的时候执行.


/**
 1. start---<NSThread: 0x604000065d00>{number = 1, name = main}
 2. end---<NSThread: 0x604000065d00>{number = 1, name = main}
 3. 1---<NSThread: 0x600000072c40>{number = 3, name = (null)}
 4. 1---<NSThread: 0x600000072c40>{number = 3, name = (null)}
 5. operation end --- <NSThread: 0x6040002750c0>{number = 4, name = (null)}
 */
-(void)NSOperationCompletion1{
    NSLog(@"start---%@", [NSThread currentThread]);
    NSOperationQueue *oq = [NSOperationQueue new];
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
        }
    }];
    [op setCompletionBlock:^{
        NSLog(@"operation end --- %@",[NSThread currentThread]); // 打印当前线程
    }];

    [oq addOperation:op];
    NSLog(@"end---%@", [NSThread currentThread]);
}

/**
 1. start---<NSThread: 0x604000071e40>{number = 1, name = main}
 2. 1---<NSThread: 0x604000071e40>{number = 1, name = main}
 3. 1---<NSThread: 0x604000071e40>{number = 1, name = main}
 4. end---<NSThread: 0x604000071e40>{number = 1, name = main}
 5. operation end --- <NSThread: 0x60000046c480>{number = 3, name = (null)}
 */
-(void)NSOperationCompletion2{
    NSLog(@"start---%@", [NSThread currentThread]);
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
        }
    }];
    [op setCompletionBlock:^{
        NSLog(@"operation end --- %@",[NSThread currentThread]); // 打印当前线程
    }];
    [op start];
    NSLog(@"end---%@", [NSThread currentThread]);
}

通过Demo, 我们可以看出completionBlock执行的内容都是在子线程中执行的(不论是使用start直接调用, 还是添加到OperationQueue).

NSOperation的completionBlock执行是在 NSOperation的 finished 属性被设置以后. 实际中finished可能是Operation被取消cancelled, 或者真正的执行完成completed, 这两种情况需要我们自主区分, 尤其在自定义NSOperation的时候.

不论是直接start调用还是添加到OperationQueue中,不论queue是并行队列还是串行队列. NSOperation的completionBlock总是在子线程中执行.

实际NSOperation的finished属性被KVO监听, 如果一旦finished, 就执行completionBlock.

NSOperationQueue

  • NSOperation 可以调用 start 方法来执行任务,但默认是同步执行的
  • 如果将 NSOperation 添加到 NSOperationQueue(操作队列)中,系统会自动异步执行NSOperation中的操作

有两种方式将Operation添加到NSOperationQueue中:

- (void)addOperation:(NSOperation *)op;
- (void)addOperationWithBlock:(void (^)(void))block;

下面是通过多重方式将各种NSOperation添加到NSOperationQueue的demo:

/**
 1. start---<NSThread: 0x600000262840>{number = 1, name = main}
 2. end---<NSThread: 0x600000262840>{number = 1, name = main}
 3. 1---<NSThread: 0x600000661ec0>{number = 3, name = (null)}
 4. 4---<NSThread: 0x604000267800>{number = 5, name = (null)}
 5. 3---<NSThread: 0x6000006620c0>{number = 4, name = (null)}
 6. 2---<NSThread: 0x604000267b80>{number = 6, name = (null)}
 7. 4---<NSThread: 0x604000267800>{number = 5, name = (null)}
 8. 1---<NSThread: 0x600000661ec0>{number = 3, name = (null)}
 9. 3---<NSThread: 0x6000006620c0>{number = 4, name = (null)}
 10 2---<NSThread: 0x604000267b80>{number = 6, name = (null)}
 */
-(void)operationQueueDemo1{
    NSOperationQueue *oq = [[NSOperationQueue alloc]init];
    NSLog(@"start---%@", [NSThread currentThread]);

    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
        }
    }];
    NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
    PPOperation *op3 = [PPOperation new];
    [oq addOperation:op1];
    [oq addOperation:op2];
    [oq addOperation:op3];
    [oq addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"4---%@",[NSThread currentThread]); // 打印当前线程
        }
    }];
    NSLog(@"end---%@", [NSThread currentThread]);
}

/**
 1. start---<NSThread: 0x600000074c00>{number = 1, name = main}
 2. end---<NSThread: 0x600000074c00>{number = 1, name = main}
 3. 4---<NSThread: 0x600000464880>{number = 3, name = (null)}
 4. 4---<NSThread: 0x600000464880>{number = 3, name = (null)}

 */
-(void)operationQueueDemo2{
    NSOperationQueue *oq = [[NSOperationQueue alloc]init];
    NSLog(@"start---%@", [NSThread currentThread]);
    [oq addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"4---%@",[NSThread currentThread]); // 打印当前线程
        }
    }];
    NSLog(@"end---%@", [NSThread currentThread]);
}

结果OperationQueue会异步开启新线程执行添加的Operation.

NSOperationQueue设置成并发队列与串行队列

可以通过设置NSOperationQueue的maxConcurrentOperationCount属性,控制该Queue是串行执行、并发执行.

默认情况下NSOperationQueue是并发队列, 系统控制最大并发数.如果设置maxConcurrentOperationCount = 1, 那么就是串行队列, 下面是实例.

/**
 1. start---<NSThread: 0x600000071a00>{number = 1, name = main}
 2. end---<NSThread: 0x600000071a00>{number = 1, name = main}
 3. 1---<NSThread: 0x60000046d6c0>{number = 3, name = (null)}
 4. 1---<NSThread: 0x60000046d6c0>{number = 3, name = (null)}
 5. 2---<NSThread: 0x60000027ea40>{number = 4, name = (null)}
 6. 2---<NSThread: 0x60000027ea40>{number = 4, name = (null)}
 */
-(void)operationQueueDemo3{
    NSOperationQueue *oq = [[NSOperationQueue alloc]init];
    NSLog(@"start---%@", [NSThread currentThread]);

    // 设置Queue是串行队列
    [oq setMaxConcurrentOperationCount:1];
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
        }
    }];
    [oq addOperation:op1];
    [oq addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
        }
    }];
    NSLog(@"end---%@", [NSThread currentThread]);
}

结论: 串行队列 + 异步调用.

NSOperationQueue的暂停与恢复

队列的暂停是 当前正在执行的任务完成以后, 暂停后面执行的任务, 相关API:

//暂停和恢复队列(YES代表暂停队列,NO代表恢复队列)
- (void)setSuspended:(BOOL)b;

//当前状态
- (BOOL)isSuspended;

暂停和恢复的使用场合:

在tableview界面,开线程下载远程的网络界面,对UI会有影响,使用户体验变差。那么这种情况,就可以设置在用户操作UI(如滚动屏幕)的时候,暂停队列(不是取消队列),停止滚动的时候,恢复队列。

NSOperation的属性与操作设置

理解Operation的就绪状态:

举个例子,现在有2个操作:op1,op2。其中 op2 依赖于 op1, 即 op2 -> op1。现在将这2个操作添加到队列中并发执行。

  • 因为 op1 没有要依赖的任务,所以op1执行之前,就是处于ready状态的操作。
  • 而op2 是有依赖的operation,所以op2当前状态就是非ready状态.

只有进入就绪状态的ready才能被添加到queue中调度执行,或者直接调用start执行.如果是非ready状态的operation, 直接调用start方法会crash.

Operation依赖相关API

不同的NSOperation之间可以添加依赖关系, 方便我们控制操作之间的执行顺序. 相关接口如下:

  • - (void)addDependency:(NSOperation *)op:
  • - (void)removeDependency:(NSOperation *)op;
  • @property (readonly, copy) NSArray *dependencies;

[op2 addDependency:op1];这里是让op2 依赖于 op1,则先执行op1,然后执行op2.

/**
 2018-07-05 17:25:54.129570+0800 NSOperationDemo[22605:1156299] start---<NSThread: 0x60400007dc80>{number = 1, name = main}
 2018-07-05 17:25:54.130242+0800 NSOperationDemo[22605:1156299] end---<NSThread: 0x60400007dc80>{number = 1, name = main}
 2018-07-05 17:26:06.135597+0800 NSOperationDemo[22605:1156399] 1---<NSThread: 0x60400027c180>{number = 3, name = (null)}
 2018-07-05 17:26:08.137346+0800 NSOperationDemo[22605:1156399] 1---<NSThread: 0x60400027c180>{number = 3, name = (null)}
 2018-07-05 17:26:10.137834+0800 NSOperationDemo[22605:1156749] 2---<NSThread: 0x600000463e40>{number = 4, name = (null)}
 2018-07-05 17:26:12.138668+0800 NSOperationDemo[22605:1156749] 2---<NSThread: 0x600000463e40>{number = 4, name = (null)}
 */
-(void)operationDependency2{
    NSOperationQueue *oq = [[NSOperationQueue alloc]init];
    NSLog(@"start---%@", [NSThread currentThread]);

    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
        }
    }];

    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
        }
    }];

    [op2 addDependency:op1];
    [oq addOperation:op2];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [oq addOperation:op1];
    });
    NSLog(@"end---%@", [NSThread currentThread]);
}

结果解释:

op2依赖op1, 因此op2先被添加到queue中,但是op2是非ready状态,因此不会执行, 当10s以后,op1被添加到queue中, op1没有依赖, 是ready状态,会直接调度执行, 当执行完成, op2变成ready状态, op2执行.

  1. 依赖关系是单项的, 例如[A addDependency:B], 那么表示A依赖B, B不依赖A.
  2. NSOperation中的依赖关系是Operation自己内部管理的, 存储在NSOperation的属性dependency中, 与具体添加到哪个Queue无关.
  3. 多个Operation之间不要建立循环依赖, 会导致他们都不会执行.
  4. 在配置Operation的依赖是在加入到OperationQueue之前, 加入到Queue以后,配置依赖可能会失效.
  5. Operation的依赖关系控制这Operation的isReady属性.

NSOperation的优先级

NSOperation有优先级属性:

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

上边我们说过:

对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性).

/**
 2018-07-05 17:54:27.764623+0800 NSOperationDemo[23666:1216301] start---<NSThread: 0x600000079400>{number = 1, name = main}
 2018-07-05 17:54:27.765324+0800 NSOperationDemo[23666:1216301] end---<NSThread: 0x600000079400>{number = 1, name = main}
 2018-07-05 17:54:29.770351+0800 NSOperationDemo[23666:1216417] 1---<NSThread: 0x60000046a780>{number = 3, name = (null)}
 2018-07-05 17:54:29.770354+0800 NSOperationDemo[23666:1216421] 2---<NSThread: 0x60000046a900>{number = 4, name = (null)}
 2018-07-05 17:54:31.771559+0800 NSOperationDemo[23666:1216421] 2---<NSThread: 0x60000046a900>{number = 4, name = (null)}
 2018-07-05 17:54:31.771560+0800 NSOperationDemo[23666:1216417] 1---<NSThread: 0x60000046a780>{number = 3, name = (null)}
 */
-(void)operationPriority1{
    NSOperationQueue *oq = [[NSOperationQueue alloc]init];
    [oq setMaxConcurrentOperationCount:3];
    NSLog(@"start---%@", [NSThread currentThread]);

    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
        }
    }];

    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
        }
    }];

    [op1 setQueuePriority:NSOperationQueuePriorityVeryHigh];
    [op2 setQueuePriority:NSOperationQueuePriorityVeryLow];
    [oq addOperation:op2];
    [oq addOperation:op1];

    NSLog(@"end---%@", [NSThread currentThread]);
}

注意

  1. 优先级决定了都是处于 ready 状态下多个operation操作之间开始的执行顺序. 执行顺序的第一要素是isReady状态, 第二要素是他们的优先级.
  2. operation的优先级只能应用与相同的 operation queue中的 operation之间. 不同的queue中的operation不受影响.
  3. 如果queue是串行队列, operation执行顺序还是按照加入到queue的先后顺序执行.

NSOperation的CompletionBlock

实际在所有的NSOperaiton中都能设置这个属性, NSOperation会在它的主任务完成时, 回调一个completionBlock. 我们可以用 completion block 来执行一些主任务之外的工作,比如,我们可以用它来通知一些客户 operation 已经执行完毕,而并发的 operation 也可以用这个 block 来生成最终的 KVO 通知。

注意,当一个 operation 被取消时,它的 completion block 仍然会执行,所以我们需要在真正执行代码前检查一下 isCancelled 方法的返回值。另外,我们也没有办法保证 completion block 被回调时一定是在主线程,理论上它应该是与触发 isFinished 的 KVO 通知所在的线程一致的,所以如果有必要的话我们可以在 completion block 中使用 GCD 来保证从主线程更新 UI 。

completion 在NSOperation的实现是通过监听isFinishedKVO来完成调用的.因此调用的线程与isFinished被设置的线程相关. 并且在completionBlock中我们也需要判断isCancelled 来决定 Operaiton是否真正的完成.

取消NSOperation

一旦NSOperation被添加到NSOperationQueue以后, 这个NSOperation的所有权就是NSOperationQueue的, 并且不能被移除.

唯一一个控制NSOperation的操作就是cancel, 或者间接调用NSOperationQueue的cancelAllOperations.

cancel 的本质是将NSOperation的isCancelled属性设置为YES.

理论上, 我们会实时检测isCancelled属性, 在该属性被设置成YES以后, 会将isFinished设置成YES(如果是自定义的NSOperation, 这部分代码需要我们完成), 这样, KVO就会发出通知, 依赖该NSOperation的其他Operation就会将isReady属性设置成YES, 自己的 completionBlock 也会执行.

图片下载处理的实例

    //1. 创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    __block UIImage *image1 = NULL;
    //2. 创建第1个下载Operation
    NSBlockOperation *download1 = [NSBlockOperation blockOperationWithBlock:^{
        NSURL *url = [NSURL URLWithString:@"http://img.pconline.com.cn/images/photoblog/9/9/8/1/9981681/200910/11/1255259355826.jpg"];
        NSData *data = [NSData dataWithContentsOfURL:url];
        image1 = [UIImage imageWithData:data];
    }];
    __block UIImage *image2 = NULL;
    //3. 创建第2个下载Operation
    NSBlockOperation *download2 = [NSBlockOperation blockOperationWithBlock:^{
        NSURL *url = [NSURL URLWithString:@"http://pic38.nipic.com/20140228/5571398_215900721128_2.jpg"];
        NSData *data = [NSData dataWithContentsOfURL:url];
        image2 = [UIImage imageWithData:data];
    }];

    // 4. 创建下载完成以后的绘图操作
    NSBlockOperation *combine = [NSBlockOperation blockOperationWithBlock:^{
        UIGraphicsBeginImageContext(CGSizeMake(100, 100));
        [image1 drawInRect:CGRectMake(0, 0, 50, 100)];
        image1 = nil;
        [image2 drawInRect:CGRectMake(50, 0, 50, 100)];
        image2 = nil;

        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            self.imageView.image = image;
        }];
    }];

    // 5. 设置绘图操作依赖两个下载操作
    [combine addDependency:download1];
    [combine addDependency:download2];
    // 6. 开始执行任务
    [queue addOperation:download1];
    [queue addOperation:download2];
    [queue addOperation:combine];
}

自定义NSOperation

如果系统提供的两个Operation的实现类不能满足我们的需求, 我们就需要自定义NSOperation.

一般而言, 我们都是需要定义并发执行的NSOperation. 因此我们需要重写NSOperation的部分方法, 例如 main方法, demo如下:

@implementation PPOperation
-(void)main{
    // 支持取消的Operation
    if(self.isCancelled == YES){
        return;
    }

    for (int i = 0; i < 2; i++) {
        [NSThread sleepForTimeInterval:2];
        NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
    }
}

当Operation开始执行以后, 它会一直执行, 直到它的任务执行完成finished, 或者被取消cancelled为止. 在实际开发中, 我们的Operation可能需要在任何时间点取消这个操作, 可能在Operation被执行之前, 可能在Operation正在运行main函数之中的某个时间点. 因此如果我们需要自定义的Operation能够完美的支持取消操作,减少不必要的CPU消耗, 我们需要在Operation执行期间, 定期的检查isCancelled属性, 一旦Operation被cancelled, 我们就需要立即停止Operation.

一般来说, 有以下几个常规点去获取isCancelled的值:

  • 在Operation开始执行时
  • 至少在每次循环中检查一次
  • 在执行一个耗时任务之前
  • 在任何相对来说比较容易终止operation的地方

apple doc 提示 isCancelled属性非常轻量, 不会对系统造成负担.

NSOperation的cancel函数并不是立即将operation取消掉,而是设置isCancelled属性.

手动调用start并且并发执行的NSOperation

默认情况下, NSOperation直接调用start是同步执行的, 也就是说, 实际上是调用的start方法的线程中执行的任务.

如果我们需要异步执行operation, 并且又是手动执行(直接调用start), 因此我们需要完成以下步骤:

  • start: 必须重写, 所有并发执行的operation都需要重写该方法.(并且不要调用[super start]).start方法是NSOperation任务的起点, 我们可以在这里配置operatioin的执行线程以及其他的context.
  • main: 可选.通常这个方法是专门用来实现与operation关联的任务的. 尽管大多数情况,我们可以在start中实现任务, 但在main实现具体任务做到控制逻辑和业务逻辑分离也很好.(SDWebImage 直接在start中完成的控制逻辑和业务逻辑调用)
  • isExecutingisFinished: 必须. 并发NSOPeration需要配置它的执行环境, 并且对外需要支持KVO监听这两个状态.
  • isConcurrent or asynchronous: 必须. 这个属性用来标志一个operation是否是并发.

基础框架如下:

#import "PPOperation2.h"
@interface PPOperation2()
// 声明属性(父类虽然有, 但是最后重新声明)
@property (assign, nonatomic, getter = isExecuting) BOOL executing;
@property (assign, nonatomic, getter = isFinished) BOOL finished;
@end

@implementation PPOperation2

// 手动合成两个实例变量 _executing, _finished
@synthesize executing = _executing;
@synthesize finished = _finished;

- (id)init {
    self = [super init];
    if (self) {
        _executing = NO;
        _finished  = NO;
    }
    return self;
}

- (BOOL)isConcurrent {
    return YES;
}

// finished 和 excuting 的 setter 需要通过KVO对外通知.
- (void)setFinished:(BOOL)finished {
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    [self didChangeValueForKey:@"isFinished"];
}

- (void)setExecuting:(BOOL)executing {
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    [self didChangeValueForKey:@"isExecuting"];
}
@end

在重写start, main方法以及业务代码以后:

@interface PPOperation2()
// 声明属性(父类虽然有, 但是最后重新声明)
@property (assign, nonatomic, getter = isExecuting) BOOL executing;
@property (assign, nonatomic, getter = isFinished) BOOL finished;
@end

@implementation PPOperation2

// 手动合成两个实例变量 _executing, _finished, 因为父类设置成ReadOnly
@synthesize executing = _executing;
@synthesize finished = _finished;

- (id)init {
    self = [super init];
    if (self) {
        _executing = NO;
        _finished  = NO;
    }
    return self;
}

- (BOOL)isConcurrent {
    return YES;
}

// finished 和 excuting 的 setter 需要通过KVO对外通知.
- (void)setFinished:(BOOL)finished {
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    [self didChangeValueForKey:@"isFinished"];
}

- (void)setExecuting:(BOOL)executing {
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    [self didChangeValueForKey:@"isExecuting"];
}

/**
 我们这里实现控制逻辑与业务逻辑的分离.
 在start方法执行时, 也就是具体的业务代码`main`执行之前, 我们判断isCancelled方法,如果成功执行, 我们将executing设置成YES(内部包含KVO相关内容)
 */
-(void)start{
    if (self.isCancelled) {
        self.finished = YES;
        return;
    }

    [NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
    self.executing = YES;
}

/**
 具体的业务执行内容, 如果业务逻辑执行
 */
- (void)main {
    NSLog(@"Start executing %@, mainThread: %@, currentThread: %@", NSStringFromSelector(_cmd), [NSThread mainThread], [NSThread currentThread]);

    for (int i = 0; i < 2; i++) {
        // 在一次循环之前检查, 检查是否被取消
        if (self.isCancelled) {
            self.executing = NO;
            self.finished = YES;
            return;
        }

        [NSThread sleepForTimeInterval:2];
        NSLog(@"业务逻辑执行---%@",[NSThread currentThread]); // 打印当前线程
    }

    // 在所有任务完成以后. 设置NSOperation状态
    self.executing = NO;
    self.finished = YES;
    NSLog(@"Finish executing %@", NSStringFromSelector(_cmd));
}
@end

注意, 即使几个operation被cancel调用, 仍然需要手动触发isFinished的KVO. 因为当一个operation依赖其他operation的时候, 它的finished 属性会被KVO建通, 只有当它所依赖的所有的operation的isFinished被设置成YES时, 这个operation才会执行.

start 方法主要影响的是 isExecutingisFinished

完整的需要保证KVO的属性:

  • isCancelled
  • isConcurrent
  • isExecuting
  • isFinished
  • isReady
  • dependencies
  • queuePriority
  • completionBlock

SDWebImage中关于NSOperation的实践

SDWebImageDownloaderOperation就是自定义NSOperatoin:

@interface SDWebImageDownloaderOperation ()

@property (assign, nonatomic, getter = isExecuting) BOOL executing;
@property (assign, nonatomic, getter = isFinished) BOOL finished;

...

@end

@implementation SDWebImageDownloaderOperation

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

...

- (void)start {
    @synchronized (self) {
        if (self.isCancelled) {
            self.finished = YES;
            [self reset];
            return;
        }
         ...
         
         self.executing = YES;
   }
}

- (void)setFinished:(BOOL)finished {
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    [self didChangeValueForKey:@"isFinished"];
}

- (void)setExecuting:(BOOL)executing {
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    [self didChangeValueForKey:@"isExecuting"];
}

- (BOOL)isConcurrent {
    return YES;
}

- (void)cancel {
    @synchronized (self) {
        [self cancelInternal];
    }
}

// @synchronized 中完成
- (void)cancelInternal {
    if (self.isFinished) return;
    [super cancel];
    if (self.dataTask) {
        [self.dataTask cancel];
        __weak typeof(self) weakSelf = self;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:weakSelf];
        });

        // As we cancelled the task, its callback won't be called and thus won't
        // maintain the isFinished and isExecuting flags.
        if (self.isExecuting) self.executing = NO;
        if (!self.isFinished) self.finished = YES;
    }

    [self reset];
}

// 执行以后, 调用done
- (void)done {
    self.finished = YES;
    self.executing = NO;

    [self reset];
}

#pragma mark NSURLSessionTaskDelegate
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    // 使用 self.dataTask = nil, delegate线程写入 dataTask
    @synchronized(self) {
        self.dataTask = nil;
        __weak typeof(self) weakSelf = self;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:weakSelf];
            if (!error) {
                [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:weakSelf];
            }
        });
    }
    ...
}

...

@end

其中整个框架就是我们自定义NSOperaiton需要去实现的内容.并且有以下知识点值得学习:

  1. start, cancel, 内部都用@synchronized 包装, 防止外部多个线程调用该方法.(实际场景中, 可能出现多个地方同时调用cancel的情况). 并且在cancel中有调用[self.dataTask cancel], 该方法会触发URLSession:task:didCompleteWithError回调方法, 内部因此也需要调用 @synchronized(self) {}包装dataTask相关的处理, 这里会有多线程风险(SDWebImageDownloadStopNotification只能执行一次.)
  2. 只在start方法中判断isCancelled
  3. 在所有完成的地方调用done, 修改 excuting/finish属性.
  4. 在结果处理中, 用synchronized保护结果, 不受race condition影响.

关于下载库, 后面都可以参考SDWebImage中Downloader.

AFNetworking 2.X版本中的NSOperation

typedef NS_ENUM(NSInteger, AFOperationState) {
    AFOperationPausedState      = -1,
    AFOperationReadyState       = 1,
    AFOperationExecutingState   = 2,
    AFOperationFinishedState    = 3,
};


static inline NSString * AFKeyPathFromOperationState(AFOperationState state) {
    switch (state) {
        case AFOperationReadyState:
            return @"isReady";
        case AFOperationExecutingState:
            return @"isExecuting";
        case AFOperationFinishedState:
            return @"isFinished";
        case AFOperationPausedState:
            return @"isPaused";
        default: {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunreachable-code"
            return @"state";
#pragma clang diagnostic pop
        }
    }
}

@interface AFURLConnectionOperation ()
@property (readwrite, nonatomic, assign) AFOperationState state;
@property (readwrite, nonatomic, strong) NSRecursiveLock *lock;

...

- (void)operationDidStart;
- (void)finish;
- (void)cancelConnection;
@end

@implementation AFURLConnectionOperation


- (instancetype)initWithRequest:(NSURLRequest *)urlRequest {
    NSParameterAssert(urlRequest);

    self = [super init];
    if (!self) {
        return nil;
    }

    _state = AFOperationReadyState;
    return self;
}

... 


- (BOOL)isReady {
    return self.state == AFOperationReadyState && [super isReady];
}

- (BOOL)isExecuting {
    return self.state == AFOperationExecutingState;
}

- (BOOL)isFinished {
    return self.state == AFOperationFinishedState;
}

- (BOOL)isConcurrent {
    return YES;
}

- (BOOL)isPaused {
    return self.state == AFOperationPausedState;
}

- (void)setState:(AFOperationState)state {
    if (!AFStateTransitionIsValid(self.state, state, [self isCancelled])) {
        return;
    }

    [self.lock lock];
    NSString *oldStateKey = AFKeyPathFromOperationState(self.state);
    NSString *newStateKey = AFKeyPathFromOperationState(state);

    [self willChangeValueForKey:newStateKey];
    [self willChangeValueForKey:oldStateKey];
    _state = state;
    [self didChangeValueForKey:oldStateKey];
    [self didChangeValueForKey:newStateKey];
    [self.lock unlock];
}

- (void)resume {
    if (![self isPaused]) {
        return;
    }

    [self.lock lock];
    self.state = AFOperationReadyState;

    [self start];
    [self.lock unlock];
}

- (void)setCompletionBlock:(void (^)(void))block {
    [self.lock lock];
    if (!block) {
        [super setCompletionBlock:nil];
    } else {
        __weak __typeof(self)weakSelf = self;
        [super setCompletionBlock:^ {
            __strong __typeof(weakSelf)strongSelf = weakSelf;

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
            dispatch_group_t group = strongSelf.completionGroup ?: url_request_operation_completion_group();
            dispatch_queue_t queue = strongSelf.completionQueue ?: dispatch_get_main_queue();
#pragma clang diagnostic pop

            dispatch_group_async(group, queue, ^{
                block();
            });

            dispatch_group_notify(group, url_request_operation_completion_queue(), ^{
                [strongSelf setCompletionBlock:nil];
            });
        }];
    }
    [self.lock unlock];
}

// start 方法也需要加锁
- (void)start {
    [self.lock lock];
    if ([self isCancelled]) {
        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    } else if ([self isReady]) {
        self.state = AFOperationExecutingState;

        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }
    [self.lock unlock];
}

- (void)operationDidStart {
    [self.lock lock];
    if (![self isCancelled]) {
        self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        for (NSString *runLoopMode in self.runLoopModes) {
            [self.connection scheduleInRunLoop:runLoop forMode:runLoopMode];
            [self.outputStream scheduleInRunLoop:runLoop forMode:runLoopMode];
        }

        [self.outputStream open];
        [self.connection start];
    }
    [self.lock unlock];

    dispatch_async(dispatch_get_main_queue(), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingOperationDidStartNotification object:self];
    });
}

- (void)finish {
    [self.lock lock];
    self.state = AFOperationFinishedState;
    [self.lock unlock];

    dispatch_async(dispatch_get_main_queue(), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingOperationDidFinishNotification object:self];
    });
}

- (void)cancel {
    [self.lock lock];
    if (![self isFinished] && ![self isCancelled]) {
        [super cancel];

        if ([self isExecuting]) {
            [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
        }
    }
    [self.lock unlock];
}

- (void)cancelConnection {
    NSDictionary *userInfo = nil;
    if ([self.request URL]) {
        userInfo = @{NSURLErrorFailingURLErrorKey : [self.request URL]};
    }
    NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:userInfo];

    if (![self isFinished]) {
        if (self.connection) {
            [self.connection cancel];
            [self performSelector:@selector(connection:didFailWithError:) withObject:self.connection withObject:error];
        } else {
            // Accommodate race condition where `self.connection` has not yet been set before cancellation
            self.error = error;
            [self finish];
        }
    }
}

总结:

  1. 内部维护了一组state, 用来完成isReady, isExecuting, isFinished等关键属性. (注意, 这里没用KVO)
  2. 在关键方法中start, operationDidStart, finish, cancel方法中, 涉及到 state操作的地方.用NSRecursiveLock递归锁加锁.(所有给state赋值的地方都有加锁)
  3. 由于NSOperation状态的KVO非常重要, 因此在setState中, 维护了状态的KVO. 这里一次性完成两个状态的改变
  4. 对于completionBlock, 部分业务逻辑需要在里面完成, 里需重写实现setCompletionBlock

注意: AFNetworking 新版本已经弃用. 改成NSURLSession了.
在我们自定义的NSOperation中, 也可以完成自定义 state, 用这种方式完成当前的需求.

参考文章

  1. iOS多线程:『NSOperation、NSOperationQueue』详尽总结
  2. 知其然亦知其所以然--NSOperation并发编程
  3. 4.4 多线程进阶篇<下>(NSOperation)
  4. iOS 并发编程之 Operation Queues
  5. NSOperation, NSOperationQueue 原理探析
  6. iOS并行开发:从NSOperation和调度队列开始
  7. WWDC 2015 - Session 226 - Advanced NSOperations

参考demo

https://github.com/brownfeng/NSOperationDemo

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

推荐阅读更多精彩内容