iOS多线程之NSOperation

NSOperation、NSOperationQueue 是基于 GCD 更高一层的封装,完全面向对象。但是比 GCD 更简单易用、代码可读性也更高。

1.为什么要使用 NSOperation、NSOperationQueue?

  • 可添加完成的代码块,在操作完成后执行。
  • 添加操作之间的依赖关系,方便的控制执行顺序。
  • 设定操作执行的优先级。
  • 可以设置最大并发数。
  • 可以很方便的取消一个操作的执行。
  • 使用 KVO 观察对操作执行状态的更改:isExecuteing、isFinished、isCancelled


2.常用类

既然是基于 GCD 的更高一层的封装。那么,GCD 中的一些概念同样适用于 NSOperation、NSOperationQueue。在 NSOperation、NSOperationQueue 中也有类似的任务(操作)和队列(操作队列)的概念。

  • 操作(Operation):
    • 执行操作的意思,换句话说就是你在线程中执行的那段代码。
    • 在 GCD 中是放在 block 中的。在 NSOperation 中,我们使用 NSOperation 子类 NSInvocationOperation、NSBlockOperation,或者自定义子类来封装操作。
  • 操作队列(Operation Queues):
    • 这里的队列指操作队列,即用来存放操作的队列。不同于 GCD 中的调度队列 FIFO(先进先出)的原则。NSOperationQueue 对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。
    • 操作队列通过设置最大并发操作数(maxConcurrentOperationCount)来控制并发、串行。
    • NSOperationQueue 为我们提供了两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。


3.NSOperation的使用

NSOperation 是个抽象类,不能用来封装操作。我们只有使用它的子类来封装操作。我们有三种方式来封装操作。

⚠️在不使用 NSOperationQueue,单独使用 NSOperation 的情况下,是 同步操作
⚠️如果自定义继承自 NSOperation 的子类,重写start方法在子线程中进行,可以实现异步操作

1). 使用子类 NSInvocationOperation
// 1.创建 NSInvocationOperation 对象
 NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget: self 
                                                                  selector: @selector(task1) 
                                                                    object: nil];
// 2.调用 start 方法开始执行操作
[op start];

在没有使用 NSOperationQueue、在主线程中单独使用使用子类 NSInvocationOperation 执行一个操作的情况下,操作是在当前线程执行的,并没有开启新线程。

2). 使用子类 NSBlockOperation
// 1.创建 NSBlockOperation 对象
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
    }];
// 2.调用 start 方法开始执行操作
    [op start];

在没有使用 NSOperationQueue、在主线程中单独使用 NSBlockOperation 执行一个操作的情况下,操作是在当前线程执行的,并没有开启新线程。

NSBlockOperation还可以添加额外的操作:

    [op addExecutionBlock:^{
 
    }];
    [op addExecutionBlock:^{
  
    }];
    [op start];

使用子类 NSBlockOperation,并调用方法 AddExecutionBlock:的情况下,blockOperationWithBlock:方法中的操作 和addExecutionBlock:中的操作有可能均在新开启的线程中进行。
NSBlockOperation 是否开启新线程,取决于操作的个数。如果添加的操作的个数多,就会自动开启新线程。当然开启的线程数是由系统来决定的。

3). 自定义继承自 NSOperation 的子类,通过实现内部相应的方法来封装操作。

我们可以自定义非并发和并发两种不同类型的 NSOperation 子类。

从最低限度上来说,每一个 operation 都应该至少实现以下两个方法:

  • 一个自定义的初始化方法,将创建的 operation 置于一个已知的状态
  • 重写 main 方法来执行我们的任务

例如:

- (id)initWithData:(id)data {
    if ([super init]) {
    }
    return self;
}
- (void)main {
}
自定义NSOperation支持取消

当一个 operation 开始执行后,它会一直执行它的任务直到完成或被取消为止。我们可以在任意时间点取消一个 operation ,甚至是在它还未开始执行之前。
为了让我们自定义的 operation 能够支持取消事件,我们需要在代码中定期地检查isCancelled方法的返回值,一旦检查到这个方法返回 YES ,我们就需要立即停止执行接下来的任务。

通常来说,当我们自定义一个 operation 类时,我们需要考虑在以下几个关键点检查 isCancelled 方法的返回值:

  • 在真正开始执行任务之前;
  • 至少在每次循环中检查一次,而如果一次循环的时间本身就比较长的话,则需要检查得更加频繁;
  • 在任何相对来说比较容易中止 operation 的地方。

看到这里,我想你应该可以意识到一点,那就是尽管 operation 是支持取消操作的,但却并不是立即取消的,而是在你调用了 operation 的 cancel 方法之后的下一个 isCancelled 的检查点取消的。

- (void)main {
     if (self.isCancelled) return;
        for (NSUInteger i = 0; i < 3; i++) {
            if (self.isCancelled) return;
            NSLog(@"Loop %@", @(i + 1));
      }
}
配置并发的自定义NSOperation

在 operation 和 operation queue 结合使用时,operation queue 可以为非并发的 operation 提供线程,但是,如果你想要手动地执行一个 operation ,又想这个 operation 能够异步执行的话,你需要做一些额外的配置来让你的 operation 支持并发执行。
下面列举了一些你可能需要重写的方法:

  • start必须的
    所有并发执行的 operation 都必须要重写这个方法,替换掉 NSOperation 类中的默认实现。start 方法是一个 operation 的起点,我们可以在这里配置任务执行的线程或者一些其它的执行环境。
    另外,需要特别注意的是,在我们重写的 start 方法中一定不要调用父类的实现;
  • main可选的
    通常这个方法就是专门用来实现与该 operation 相关联的任务的。尽管我们可以直接在 start 方法中执行我们的任务,但是用 main 方法来实现我们的任务可以使设置代码和任务代码得到分离,从而使 operation 的结构更清晰;
  • isExecutingisFinished必须的
    并发执行的 operation 需要负责配置它们的执行环境,并且向外界客户报告执行环境的状态。因此,一个并发执行的 operation 必须要维护一些状态信息,用来记录它的任务是否正在执行,是否已经完成执行等。
    此外,当这两个方法所代表的值发生变化时,我们需要生成相应的 KVO 通知,以便外界能够观察到这些状态的变化;
  • isConcurrent必须的
    这个方法的返回值用来标识一个 operation 是否是并发的 operation ,我们需要重写这个方法并返回 YES 。
    例如:
@synthesize executing = _executing;
@synthesize finished  = _finished;

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

- (BOOL)isConcurrent {
    return YES;
}

- (BOOL)isExecuting {
    return _executing;
}

- (BOOL)isFinished {
    return _finished;
}

- (void)start {
    if (self.isCancelled) {
        [self willChangeValueForKey:@"isFinished"];
        _finished = YES;
        [self didChangeValueForKey:@"isFinished"];
        
        return;
    }
    
    [self willChangeValueForKey:@"isExecuting"];
    
    [NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
    _executing = YES;
    
    [self didChangeValueForKey:@"isExecuting"];
}
// 任务真正开始执行
- (void)main {
    @try {
        NSLog(@"Start executing %@, mainThread: %@, currentThread: %@", NSStringFromSelector(_cmd), [NSThread mainThread], [NSThread currentThread]);
        
        [self willChangeValueForKey:@"isExecuting"];
        _executing = NO;
        [self didChangeValueForKey:@"isExecuting"];
        
        [self willChangeValueForKey:@"isFinished"];
        _finished  = YES;
        [self didChangeValueForKey:@"isFinished"];
        
        NSLog(@"Finish executing %@", NSStringFromSelector(_cmd));
    }
    @catch (NSException *exception) {
        NSLog(@"Exception: %@", exception);
    }
}

注意:

  • 与重写 main 方法不同的是,如果我们重写了 start 方法或者对 NSOperation 类做了大量定制的话,我们需要保证自定义的 operation 在这些 key paths 上仍然支持 KVO 通知。比如,当我们重写了 start 方法时,我们需要特别关注的是 isExecuting 和 isFinished 这两个 key paths ,因为这两个 key paths 最可能受重写 start 方法的影响。
  • 即使一个 operation 是被 cancel 掉了,我们仍然需要手动触发 isFinished 的 KVO 通知。
    因为当一个 operation 依赖其他 operation 时,它会观察所有其他 operation 的 isFinished 的值的变化,只有当它依赖的所有 operation 的 isFinished 的值为 YES 时,这个 operation 才能够开始执行。
    因此,如果一个我们自定义的 operation 被取消了但却没有手动触发 isFinished 的 KVO 通知的话,那么所有依赖它的 operation 都不会执行。

NSOperation 类的以下 key paths 支持 KVO 通知,我们可以通过观察这些 key paths 非常方便地监听到一个 operation 内部状态的变化:

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

附:
虽然我们可以同时使用NSOperation和NSOperationQueue来创建并发任务,
但是我们也可以单独使用NSOperation实现:
创建NSOperation子类,并且在start方法中判断当前Operation是否是并发操作

- (void)start {
    if (operation.isCancelled) {
        ranIt = YES;
    } else if (operation.isReady) {
        if (!operation.isConcurrent) {
            [operation start];
        } else {
            [NSThread detachNewThreadSelector:@selector(start) toTarget:operation withObject:nil];
        }
        ranIt = YES;
    }
}
4).设置CompletionBlock

一个 operation 可以在它的主任务执行完成时回调一个 completion block 。我们可以用 completion block 来执行一些主任务之外的工作,比如,我们可以用它来通知一些客户 operation 已经执行完毕,而并发的 operation 也可以用这个 block 来生成最终的 KVO 通知。如果需要设置一个 operation 的 completion block ,直接调用 NSOperation 类的 setCompletionBlock: 方法即可。

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

5).取消、暂停

一旦一个 operation 被添加到 operation queue 后,唯一从 operation queue 中出队一个 operation 的方式就是调用它的 cancel 方法取消这个 operation ,或者直接调用 operation queue 的 cancelAllOperations 方法取消这个 operation queue 中所有的 operation 。
另外,我们前面也提到了,当一个 operation 被取消后,这个 operation 的 isFinished 状态也会变成 YES ,这样处理的好处就是所有依赖它的 operation 能够接收到这个 KVO 通知,从而能够清除这个依赖关系正常执行。

如果我们想要暂停和恢复执行 operation queue 中的 operation ,可以通过调用 operation queue 的 setSuspended:方法来实现这个目的。不过需要注意的是,暂停执行 operation queue 并不能使正在执行的 operation 暂停执行,而只是简单地暂停调度新的 operation 。另外,我们并不能单独地暂停执行一个 operation ,除非直接 cancel 掉。



4.NSOperationQueue和NSOperation配合使用

1). 设置最大并发数

NSOperationQueue的属性 maxConcurrentOperationCount最大并发操作数,用来控制一个特定队列中可以有多少个操作同时参与并发执行。

  • maxConcurrentOperationCount 默认情况下为-1,表示不进行限制,可进行并发执行。
  • maxConcurrentOperationCount 为1时,队列为串行队列。只能串行执行。
  • maxConcurrentOperationCount 大于1时,队列为并发队列。操作并发执行,当然这个值不应超过系统限制,即使自己设置一个很大的值,系统也会自动调整为 min{自己设定的值,系统设定的默认最大值}。
// 创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    queue.maxConcurrentOperationCount = 1; // 串行队列
 // queue.maxConcurrentOperationCount = 2; // 并发队列
 // queue.maxConcurrentOperationCount = -1; // 默认值, 并发队列

    [queue addOperationWithBlock:^{

    }];
    [queue addOperationWithBlock:^{

    }];
2).设置操作依赖
// 1.创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    // 2.创建操作
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
    }];
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
    }];

    // 3.添加依赖
    [op2 addDependency:op1]; // 让op2 依赖于 op1,则先执行op1,在执行op2
    // 4.添加操作到队列中
    [queue addOperation:op1];
    [queue addOperation:op2];

3).设置优先级

NSOperation 提供了queuePriority(优先级)属性,queuePriority属性适用于 同一操作队列 中的操作,不适用于不同操作队列中的操作。

当一个操作的所有依赖都已经完成时,即isReady为YES,操作对象通常会进入准备就绪状态,等待执行。
queuePriority属性决定了进入准备就绪状态下的操作之间的开始执行顺序。并且,优先级不能取代依赖关系。

  • 如果一个队列中既包含高优先级操作,又包含低优先级操作,并且两个操作都已经准备就绪,那么队列先执行高优先级操作。

  • 如果,一个队列中既包含了准备就绪状态的操作,又包含了未准备就绪的操作,未准备就绪的操作优先级比准备就绪的操作优先级高。那么,虽然准备就绪的操作优先级低,也会优先执行。优先级不能取代依赖关系。如果要控制操作间的启动顺序,则必须使用依赖关系。



5.线程间通信

// 1.创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];

    // 2.添加操作
    [queue addOperationWithBlock:^{
        // 异步进行耗时操作

        // 回到主线程
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            // 进行一些 UI 刷新等操作
        }];
    }];

你可能感兴趣的:(iOS多线程之NSOperation)