iOS多线程编程(六) NSOperation

897 阅读27分钟

多线程系列篇章计划内容:
iOS多线程编程(一) 多线程基础
iOS多线程编程(二) Pthread
iOS多线程编程(三) NSThread
iOS多线程编程(四) GCD
iOS多线程编程(五) GCD的底层原理
iOS多线程编程(六) NSOperation
iOS多线程编程(七) 同步机制与锁
iOS多线程编程(八) RunLoop

iOS下的多线程技术方案如图所示: 在前面几章中,我们已将pthreadNSThread以及GCD尽可能详细地进行了介绍,今天我们来认识:NSOperation

1.NSOperation 简介

NSOperation 是iOS2.0推出的,最早是通过NSThread实现,在iOS4.0推出GCD之后,苹果又重写了NSOperation

自此,NSOperation 是一套基于GCD封装的、面向对象的多线程解决方案。

我们不妨先回顾一下GCD的核心概念。

在GCD中,我们说GCD的三要素是:『任务』、『队列』和『函数』。其中『任务』就是线程要执行的具体操作;『队列』是用来存储任务的,不同的队列(串行/并发)会影响任务的调度。而『函数』决定了任务的执行方式,同步执行还是异步执行;

既然是对GCD的更高一层的封装,那么这些概念也同样适用于NSOperation。在NSOperation中,『任务』对应一个NSOperation(操作对象),一个操作对象封装了你想要执行的工作;『队列』对应的是 NSOperationQueue(操作队列),操作队列用来保存并管理操作对象。对于任务的执行方式,取决于对操作对象操作队列的设置。

相较于GCD,NSOperation的优势体现在:

NSOperation中,『任务』被包装成一个对象,并拓展了属性与方法,为操作任务提供了可能,我们可以更简单方便地管理和控制任务,如:

  • 监测任务的执行状态;
  • 可添加完成的代码块,在任务完成后执行;
  • 取消未执行的任务;
  • 使用 KVO 对任务执行状态的更改:isExecuteing、isFinished、isCancelled;
  • 添加任务之间的依赖关系,方便的控制执行顺序。
  • 设定任务执行的优先级。

使用GCD也可实现这些功能,但相对复杂。

而由于是对GCD的进一步封装,或多或少会带来一些额外的系统开销

所以对于简单的多线程任务,更建议使用GCD,而对于任务的状态跟踪要求较高、任务存在依赖关系以及优先级等,使用NSOperation会是更好的选择。

2. NSOperation 的基本使用

有两种方式可以让NSOperation执行任务:

  • 方式一:单独使用NSOperation: 将需要执行的任务封装到NSOperation的子类对象中,之后调用start方法。
    默认情况下,此方式是在当前线程同步执行
  • 方式二: 将NSOperation添加到NSOperationQueue中: 将需要执行的任务封装到NSOperation的子类对象中,并添加到NSOperationQueue,在这之后,操作对象由操作队列管理。
    默认情况下,此方式是异步并发的。

2.1 单独使用NSOperation

无论何种方式,我们都需要使用NSOperation对象,并且需要使用其子类对象。

这是因为:NSOperation是表示与单个任务关联的代码和数据的抽象类。不具备封装操作的能力。在使用时,必须使用其子类。尽管是抽象的,但NSOperation的基础实现中已经做好了重要的逻辑工作,来协调任务的安全执行,以最小化必须在自己的子类中完成的工作量。另外,Foundation框架还提供了两个具体的子类 NSInvocationOperationNSBlockOperation

所以,有三种方式使用NSOperation:

  • 使用系统子类:NSInvocationOperation
  • 使用系统子类:NSBlockOperation
  • 自定义继承自NSOperation的子类,根据需要实现内部逻辑

2.1.1 NSInvocationOperation

NSInvocationOperation提供了两种创建操作对象的方式:

  • initWithTarget:selector:object:
  • initWithInvocation:

创建完成后,均需要调用start方法,启动操作对象执行任务。

使用示例如下:

/**
 * 使用系统子类 NSInvocationOperation
 */
- (void)useInvocationOperation {
    
    // 使用 initWithTarget:selector:object: 示例
    // 1.创建 NSInvocationOperation 对象
    NSInvocationOperation *iop = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task) object:nil];
    NSLog(@"target_begin");
    // 2.调用 start 方法使线程进入就绪状态
    [iop start];
    NSLog(@"target_end");
    
    NSLog(@"---------------");
    
    // 使用 initWithInvocation: 示例
    // 1.创建 NSInvocationOperation 对象
    NSMethodSignature *sign = [[self class] instanceMethodSignatureForSelector:@selector(task)];
    NSInvocation *inv = [NSInvocation invocationWithMethodSignature:sign];
    inv.target = self;
    inv.selector = @selector(task);
    NSInvocationOperation *iop2 = [[NSInvocationOperation alloc] initWithInvocation:inv];
    NSLog(@"invocation_begin");
    // 2.调用 start 方法使线程进入就绪状态
    [iop2 start];
    NSLog(@"invocation_end");
}

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

打印结果:

target_begin
任务---<NSThread: 0x600000db00c0>{number = 1, name = main}
任务---<NSThread: 0x600000db00c0>{number = 1, name = main}
target_end
--------------
invocation_begin
任务---<NSThread: 0x600000db00c0>{number = 1, name = main}
任务---<NSThread: 0x600000db00c0>{number = 1, name = main}
invocation_end

为了区分是在主线程执行任务还是当前线程执行任务,我们可以将useInvocationOperation方法在子线程中调用。

[self performSelectorInBackground:@selector(useInvocationOperation) withObject:nil];

打印结果如下:

target_begin
任务---<NSThread: 0x600000b16480>{number = 7, name = (null)}
任务---<NSThread: 0x600000b16480>{number = 7, name = (null)}
target_end
--------------
invocation_begin
任务---<NSThread: 0x600000b16480>{number = 7, name = (null)}
任务---<NSThread: 0x600000b16480>{number = 7, name = (null)}
invocation_end

可见,默认情况下,单独使用NSInvocationOperation执行的任务,会在当前线程同步执行

NSBlockOperation子类不同的是,因为没有额外添加任务的方法,使用NSInvocationOperation创建的对象只会有一个任务。

2.1.2 NSBlockOperation

NSBlockOperation有两种创建操作对象的方式:

  • - (instancetype)init
  • + (instancetype)blockOperationWithBlock:(void (^)(void))block; 第二种创建方式是类方法,可以通过该方法初始化一个带有执行任务的NSBlockOperation对象,block中即是要执行的任务。

另外还提供了一个追加任务的方法:

  • - (void)addExecutionBlock:(void (^)(void))block; 使用此方法,可以给一个已经存在的NSBlockOperation对象追加额外的任务。这个对象可以是通过blockOperationWithBlock创建的,也可以是通过init方法初始化的。

单独使用NSBlockOperation时,还是需要调用start方法来启动操作对象执行任务。

使用示例如下:

/**
 * 使用系统子类 NSBlockOperation
 * blockOperationWithBlock方式
 */
- (void)useBlockOperation {
    
    // 1.创建 NSBlockOperation 对象
    NSBlockOperation *bop = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];             // 模拟耗时操作
            NSLog(@"任务1---%@", [NSThread currentThread]); // 打印当前线程
        }
    }];
 
    NSLog(@"begin");
    // 2.调用 start 方法使线程进入就绪状态
    [bop start];
    NSLog(@"end");
}

打印结果:

begin
任务1---<NSThread: 0x60000169c3c0>{number = 1, name = main}
任务1---<NSThread: 0x60000169c3c0>{number = 1, name = main}
end

同样为了区分具体是如何在线程执行,我们将useBlockOperation 放在子线程执行,结果也如同在子线程中执行useInvocationOperation 一样,任务会在当前子线程中执行。

由此,使用NSBlockOperation执行1个任务的时候,会在当前线程同步执行任务。

为什么会强调执行1个任务呢?

如果使用addExecutionBlock:,在上例中追加额外任务

/**
 * 使用系统子类 NSBlockOperation
 * 并通过 AddExecutionBlock: 追加任务
 */
- (void)useBlockOperationAndAddExecutionBlock {
    
    // 1.创建 NSBlockOperation 对象
    NSBlockOperation *bop = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"任务1---%@", [NSThread currentThread]); // 打印当前线程
        }
    }];
    
    // 2.追加任务2
    [bop addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"任务2---%@", [NSThread currentThread]); // 打印当前线程
        }
    }];
    // 3.追加任务3
    [bop addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"任务3---%@", [NSThread currentThread]); // 打印当前线程
        }
    }];
    
    NSLog(@"begin");
    // 4.用 start 方法使线程进入就绪状态
    [bop start];
    NSLog(@"end");
}

打印结果如下:

begin
任务2---<NSThread: 0x600000d8e100>{number = 4, name = (null)}
任务3---<NSThread: 0x600000dc8600>{number = 1, name = main}
任务1---<NSThread: 0x600000df7a80>{number = 6, name = (null)}
任务2---<NSThread: 0x600000d8e100>{number = 4, name = (null)}
任务1---<NSThread: 0x600000df7a80>{number = 6, name = (null)}
任务3---<NSThread: 0x600000dc8600>{number = 1, name = main}
end

可见,如果使用addExecutionBlock:追加任务,则这些任务(包括blockOperationWithBlock 中的任务)可以在多个不同线程中并发(同时)执行,但是操作对象本身依然是同步执行的,所以会阻塞当前线程,直到所有的操作全部完成。

不同于NSInvocationOperationNSBlockOperation对象不用添加到操作队列中也能开启新线程,前提是一个NSBlockOperation对象中封装了多个任务。

  • 任务数为1时,不会开启新线程,在当前线程同步执行
  • 任务数大于1时,会开启新线程,各任务并发执行
  • 不管任务数是否大于1,对于操作对象而言,都是同步执行的。

2.1.3 自定义NSOperation

如果NSInvocationOperationNSBlockOperation不够使用,希望可以控制更多的状态或者封装一个可以重复利用的NSOperation子类,就可以自定义NSOperation,自定义NSOperation涉及到一些状态上的管理,我们在了解完 3.NSOperation的进阶使用 后再进行定义。

2.2 结合NSOperationQueue 使用

单独使用NSOperation对象,通过start方法开启的操作,默认情况下,大多是当前线程同步执行的,尽管NSBlockOperation可以开启子线程,但是仍然会阻塞当前线程,直到操作全部完成。这并不是真正意义上的多线程编程,要想使用NSOperation实现多线程编程,我们需要配合使用NSOperationQueue

NSOperationQueue 提供了两种队列:主队列自定义队列

  • 主队列:可通过[NSOperationQueue mainQueue]获取主队列,凡是加入主队列的操作都会在主线程执行,如同GCD中的dispatch_get_main_queue
  • 自定义队列:通过[[NSOperationQueue alloc] init]创建的队列就是自定义队列,加入到自定义队列的操作会自动放到子线程中执行,且同时包含了串行并发的能力。

使用将NSOperation对象添加NSOperationQueue中,来管理操作对象是非常方便的。因为当我们把操作对象添加到操作队列后,该NSOperationQueue对象从队列中拿取操作、以及分配到对应线程的工作都是由系统处理的。

NSOperation和NSOperationQueue实现多线程的步骤:

  • 定义操作:将需要执行的操作封装到一个 NSOperation 对象中,根据需要对操作对象进行配置;
  • 定义操作队列:创建一个 NSOperationQueue 对象,配置队列相关属性;
  • 添加操作到操作队列中:然后将 NSOperation 对象添加到 NSOperationQueue 队列中。系统会自动将 NSOperationQueue 中的 NSOperation 取出来,将取出的 NSOperation 封装的操作放到一条新线程上执行。

只要是创建了队列,在队列中的操作,就会在子线程中执行,并且默认并发操作。添加到子队列NSOperationQueue实例中的操作,都是异步执行

NSOperationQueue的特性: 不同于Dispatch Queue总是严格的按照FIFO(先进先出)的顺序执行任务,Operation Queue在确定任务的执行顺序时考虑了其他因素,如:任务间的依赖关系和优先级(后面会讲)。 从表象上看,操作队列更像是一个Pool,而非Queue。

2.2.1 添加操作到操作队列

有三种方式可以将操作加入操作队列中:

  • - (void)addOperation:(NSOperation *)op;
  • - (void)addOperationWithBlock:(void (^)(void))block;
  • - (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait;

addOperation:的方式可以将已创建好的操作对象,添加到操作队列中,此种方式便于设置操作对象的相关属性等;
addOperationWithBlock: 的方式无需事先创建操作对象,系统自动封装成一个NSBlockOperation对象,然后添加到队列中。此种方式简单,但是无法设置操作对象;
addOperations:waitUntilFinished:的方式可以添加多个操作对象到队列中,wait参数用来标识是否需要阻塞线程,等待操作的完成。

使用示例代码如下:

方式一:- (void)addOperation:(NSOperation *)op;

/**
 * 使用 addOperation: 将操作加入到操作队列中
 */
- (void)addOperationToQueue {
    
    // 1.创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    // 2.创建操作
    // 2.1使用 NSInvocationOperation 创建operation1
    NSInvocationOperation *iop = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];

    // 2.2使用 NSBlockOperation 创建 operation2
    NSBlockOperation *bop = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"任务2---%@", [NSThread currentThread]); // 打印当前线程
        }
    }];
    // 追加任务3 到 bop
    [bop addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"任务3---%@", [NSThread currentThread]); // 打印当前线程
        }
    }];
    NSLog(@"begin");
    // 3.使用 addOperation: 添加所有操作到队列中
    [queue addOperation:iop]; // [iop start]
    [queue addOperation:bop]; // [bop start]
    
    NSLog(@"end");
}

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

打印结果:

begin
end
任务2---<NSThread: 0x600000088c00>{number = 5, name = (null)}
任务1---<NSThread: 0x6000000f2e40>{number = 8, name = (null)}
任务3---<NSThread: 0x6000000fa600>{number = 6, name = (null)}
任务2---<NSThread: 0x600000088c00>{number = 5, name = (null)}
任务3---<NSThread: 0x6000000fa600>{number = 6, name = (null)}
任务1---<NSThread: 0x6000000f2e40>{number = 8, name = (null)}

方式二:- (void)addOperationWithBlock:(void (^)(void))block;

/**
 * 使用 addOperationWithBlock: 将操作加入到操作队列中
 */

- (void)addOperationWithBlockToQueue {
    NSLog(@"begin");
    // 1.创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    // 2.使用 addOperationWithBlock: 添加操作到队列中
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"任务1---%@", [NSThread currentThread]); // 打印当前线程
        }
    }];
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"任务2---%@", [NSThread currentThread]); // 打印当前线程
        }
    }];
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"任务3---%@", [NSThread currentThread]); // 打印当前线程
        }
    }];
    NSLog(@"end");
}

打印结果:

begin
end
任务1---<NSThread: 0x600001fe7240>{number = 4, name = (null)}
任务2---<NSThread: 0x600001f9a140>{number = 3, name = (null)}
任务3---<NSThread: 0x600001fe7980>{number = 2, name = (null)}
任务2---<NSThread: 0x600001f9a140>{number = 3, name = (null)}
任务1---<NSThread: 0x600001fe7240>{number = 4, name = (null)}
任务3---<NSThread: 0x600001fe7980>{number = 2, name = (null)}

方式三:- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait;

我们将方式一的示例代码的添加方式更改为addOperations:waitUntilFinished: ,且wait参数设置为NO;

/**
 * 使用 addOperations:waitUntilFinished: 将多个操作加入到操作队列中
 */

- (void)addOperationswaitUntilFinishedToQueue {
    // 1.创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    // 2.创建操作
    // 2.1使用 NSInvocationOperation 创建operation1
    NSInvocationOperation *iop = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
    
    // 2.1使用 NSBlockOperation 创建 operation2
    NSBlockOperation *bop = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"任务2---%@", [NSThread currentThread]); // 打印当前线程
        }
    }];
    // 追加任务3 到 bop
    [bop addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"任务3---%@", [NSThread currentThread]); // 打印当前线程
        }
    }];
    NSLog(@"begin");
    
//    [queue addOperation:iop]; // [iop start]
//    [queue addOperation:bop]; // [bop start]
    // 3.使用 addOperations:waitUntilFinished: 添加所有操作到队列中
    [queue addOperations:@[iop,bop] waitUntilFinished:NO];
    NSLog(@"end");
}

wait参数设置为NO时,其结果同方式一,都是异步并发执行的, 如果将wait参数设置为YES,各任务是并发的,但对于操作而言是同步的,结果如下:

begin
任务3---<NSThread: 0x6000007b1a00>{number = 5, name = (null)}
任务1---<NSThread: 0x6000007b1000>{number = 6, name = (null)}
任务2---<NSThread: 0x6000007b5f00>{number = 4, name = (null)}
任务2---<NSThread: 0x6000007b5f00>{number = 4, name = (null)}
任务1---<NSThread: 0x6000007b1000>{number = 6, name = (null)}
任务3---<NSThread: 0x6000007b1a00>{number = 5, name = (null)}
end

通过将操作添加到操作队列,方式一、方式二、以及方法三中的wait参数设为NO时,任务是在多个子线程中并发执行,并且是异步执行的,并没有阻塞当前线程。虽然方式三中,wait参数设为YES时,是同步执行的,但此种情况是我们的特殊设置,且并不是默认情况。

3.NSOperation 的进阶使用

NSOperation 的基本使用中,我们可以利用NSOperation 实现简单的多线程编程,但实际情况中,多线程编程往往存在很多复杂的情况。我们在使用GCD实现多线程编程时,需要利用栅栏函数、调度组、信号量来实现多线程编程中的复杂需求。那对于NSOperation,我们如何实现这些复杂的需求呢?

我们先从NSOperation对象的状态来了解。

3.1 NSOperation 对象的生命周期

当我们初始化一个Operation对象时,它的初始状态是新建态(Pending),Operation对象调用start方法后,在某一时刻会到就绪态(Ready),这取决于任务间的依赖(后面会讲),当系统调度了已处于Ready态的Operation任务时,这时Operation从Queue中取出开始执行,进入执行态(Executing),最后执行完毕到完成态(Finished)。在除Finished态以外的其它三个状态都可以到取消态(Canceled)。

  • Ready

一个Operation对象只有在Ready状态下才可以被调度执行。就如同单独使用NSOperation执行任务时不调用start方法,任务是不会执行的(加入操作队列中的操作对象也是在适当的时机隐式地调用了start方法)。Operation的Ready状态与其被添加到Operation Queue中的顺序无关,即使是最先添加到Operation Queue中的Operation,也未必最先进入Ready状态。这也是Operation Queue的特性。

如上图,在一个串行的操作队列中,即使Operation1最早加入队列,但是Operation1依赖于Operation2,所以在此情况下,Operation2将最早进入Ready状态,并最先开始执行。

利用Ready属性我们可以确定依赖。默认条件下,如果A依赖于B,只有当B进入Finished状态,那么A才会进入Ready状态。这个依赖可以存在于不同的Operation Queue。如果一个Operaion存在依赖关系,只有它依赖的所有Operation处于Finished状态,它才会进入Ready状态。

  • Canceled

Cancel未执行的Operation,该方法会设置对象内的标志位,表明Operation不需要执行,如果此时Operation还未执行,那么就可以取消它的执行,如果Operation已经开始执行,就无法立即取消其执行了,任务还会继续执行,但会标记为Canceled。默认条件下,Operation调用cancel,也会标志它进入Finished状态。因为只有这样依赖它的Operation才有机会被执行。

Operation 的状态属性是满足KVO的,可以被监听的keyPath有(isCancelled,isAsynchronous,isExecuting,isFinished,isReady等)。如果我们在自定义Operaion时候重写以上属性,也要确保它们可以被KVO和KVC。

3.2 completionBlock

@property (nullable, copy) void (^completionBlock)(void)

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

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

案例一:单独使用

- (void)completionBlock {
    NSBlockOperation *bop = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
        NSLog(@"执行任务---%@",[NSThread currentThread]);
    }];
    bop.completionBlock = ^{
        NSLog(@"任务执行完毕");
    };
    NSLog(@"begin");
    [bop start];
    NSLog(@"end");
}

// 打印结果:
begin
执行任务---<NSThread: 0x600002e64300>{number = 1, name = main}
end
任务执行完毕

案例二:配合NSOperationQueue使用

- (void)completionBlock {
    NSBlockOperation *bop = [[NSBlockOperation alloc] init];
    [bop addExecutionBlock:^{
        [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
        NSLog(@"执行任务1---%@",[NSThread currentThread]);
    }];
    [bop addExecutionBlock:^{
        [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
        NSLog(@"执行任务2---%@",[NSThread currentThread]);
    }];
    [bop addExecutionBlock:^{
        [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
        NSLog(@"执行任务3---%@",[NSThread currentThread]);
    }];
    NSLog(@"begin");
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:bop];
    bop.completionBlock = ^{
        NSLog(@"执行完毕---%@",[NSThread currentThread]);
    };
    NSLog(@"end");
}

// 打印结果:
begin
end
执行任务1---<NSThread: 0x60000302cac0>{number = 7, name = (null)}
执行任务3---<NSThread: 0x60000302dc80>{number = 5, name = (null)}
执行任务2---<NSThread: 0x60000306f700>{number = 6, name = (null)}
执行完毕 ---<NSThread: 0x60000302dc80>{number = 5, name = (null)}

Note:

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

3.3 暂停、继续与取消

  • 取消

一旦NSOperation子类操作对象添加到NSOperationQueue对象中,该队列就拥有了该操作对象并且不能删除操作对象,如果不想执行操作对象,只能取消该操作对象,将其标记为Canceled状态。

关于取消操作,可以分为2种情况,取消一个操作和取消一个队列的全部操作。

  • 调用NSOperation类实例的cancel方法取消单个操作对象。
  • 调用NSOperationQueue类实例的cancelAllOperations方法取消队列中全部操作对象。

取消(cancel)时,有 3 种情况:

  • 1.操作在队列中等待执行,这种情况下,操作将不会被执行。
  • 2.操作已经在执行中,此时,系统不会强制停止这个操作,但是,其 canceled属性会被置为 YES
  • 3.操作已完成,此时,cancel 无任何影响。

对于队列中的操作,只有操作标记为Finished才能被队列移除。在队列中未被调度的操作,会调用start方法执行操作,以便操作对象处理取消事件。然后标记这些操作对象为Finished。对于正在线程中执行其任务的操作对象,正在执行的任务会继续执行,该操作对象会被标记为Finished

注意:只会停止调度队列中操作对象,正在执行任务的依然会执行,且取消不可恢复。

  • 暂停与继续

一个操作执行还未完成时,我们可能需要让该任务暂停、在之后的某一时刻又希望继续执行。这时候就不能用取消操作了,需使用suspended属性。

对于暂停操作,当NSOperationQueue对象属性suspended设置为YES,队列会停止对任务调度。如果任务已在执行,则任务不会受影响,因其已被队列调度到一个线程上并执行。

对于继续操作,当属性suspended设置为NO会继续执行线程操作。队列将继续调度那些已处于Ready状态的操作对象。

3.4 操作同步

- (void)waitUntilFinished; // NSOperation对象方法
- (void)waitUntilAllOperationsAreFinished; // NSOperationQueue对象方法

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

除了等待单个Operation完成,你也可以同时等待一个queue中的所有操作,使用NSOperationQueuewaitUntilAllOperationsAreFinished方法。

注意:

  • 在等待一个 queue时,应用的其它线程仍然可以往queue中添加Operation,因此可能会加长线程的等待时间。
  • waitUntilAllOperationsAreFinished一定要在操作队列添加了操作后再设置。即,先向Operation Queue中添加Operation,再调用[OperationQueue waitUntilAllOperationsAreFinished]

3.5 maxConcurrentOperationCount属性

maxConcurrentOperationCount 为最大并发操作数,意为在一个特定的操作队列中,最多可以有多少个操作并发执行。

需要注意区分的是:这里的最大并发操作数,并不是控制的是并发线程的个数,而是一个队列中能“同时”并发的操作的最大个数。

可以利用该属性来限制最大并发操作数,同时也可以控制队列是串行执行或者并发执行

maxConcurrentOperationCount默认值为-1,表示不限制,可并发。由系统控制可用线程。 maxConcurrentOperationCount设置为1时,队列则为串行队列(不同于GCD的串行队列)。 maxConcurrentOperationCount设置大于1时,队列为并发队列,但是也不是毫无限制的,系统维护了一个默认的最大值,最大并发操作数不会超过系统默认的最大值。

注意:

  • 不要设置maxConcurrentOperationCount = 0
  • 不要设置最大并发数过大,3~4个为宜。

3.6 NSOperation 操作依赖

NSOperation的依赖关系是按照特定顺序执行操作的一种十分方便的方式。通过操作依赖,我们可以很方便的控制操作之间的执行先后顺序,使用起来也非常简单:

  • 通过- (void)addDependency:(NSOperation *)op;添加依赖,使调用者依赖于op的完成。
  • 通过- (void)removeDependency:(NSOperation *)op;取消依赖
  • 通过@property (readonly, copy) NSArray<NSOperation *> *dependencies;查看调用者的所有依赖

默认情况下,具有依赖关系的操作对象待其所有依赖的操作对象完成执行后才被认为准备就绪。一旦最后一个依赖操作完成,操作对象才会进入Ready状态并能够执行了。

NSOperation对象会管理自己的依赖,即使是不同队列的操作,也可以建立依赖关系,且按依赖顺序执行。也不区分依赖项操作是否成功完成(Cancel一个操作会将其标记为Finished,但操作不一定真正的完成)。

使用示例:

/**
 * 操作依赖
 */
- (void)addDependency {

    // 1.1创建队列1
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    // 1.2创建队列2
    NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
    // 2.创建操作
    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]); // 打印当前线程
        }
    }];
    NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"任务3---%@", [NSThread currentThread]); // 打印当前线程
        }
    }];

    // 3.添加依赖
    NSLog(@"begin");
    [op2 addDependency:op1]; // op2 依赖于 op1,则先执行op1,再执行op2
    [op3 addDependency:op2]; // op3 依赖于 op2,则先执行op2,再执行op3
//    op1.completionBlock = ^{
//        [op3 removeDependency:op2];
//    };
    // 4.添加操作到队列中
    [queue2 addOperation:op3];  //op3 添加到 队列2
    [queue addOperation:op1];   //op1 添加到 队列1
    [queue addOperation:op2];   //op2 添加到 队列1
    NSLog(@"end");
}

打印结果如下:

begin
end
任务1---<NSThread: 0x600001bafbc0>{number = 7, name = (null)}
任务1---<NSThread: 0x600001bafbc0>{number = 7, name = (null)}
任务2---<NSThread: 0x600001bafbc0>{number = 7, name = (null)}
任务2---<NSThread: 0x600001bafbc0>{number = 7, name = (null)}
任务3---<NSThread: 0x600001bafbc0>{number = 7, name = (null)}
任务3---<NSThread: 0x600001bafbc0>{number = 7, name = (null)}

不添加依赖的情况下,本例应是异步并发的,任务的执行也是无序的,在本例中op3依赖于op2,op2依赖于op1,所以最后的执行顺序按照任务1、任务2、任务3顺序打印,尽管操作并不同属于同一个队列。

注意:
不能添加循环的依赖,即A依赖于B,B依赖于A; 在操作被加入队列之前设置依赖关系;

3.7 NSOperation 优先级

NSOperation 提供了queuePriority(优先级)属性,代表操作在同一队列中执行的优先级,默认情况下,创建的操作对象优先级都是NSOperationQueuePriorityNormal。我们可以通过setQueuePriority:方法来改变当前操作在同一队列中的执行优先级。

// 优先级取值  从上到下依次升高
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};

注意区分 操作依赖操作优先级 的区别

两者都是操作对象的属性,但操作依赖可以跨队列添加,而优先级只能应用于相同队列中的操作对象,如果应用有多个操作队列,每个队列的优先级等级是相互独立的,因此,不同队列中的低优先级操作仍然可能先于高优先级操作执行。

那么优先级高的操作一定先执行吗?

未必,在操作对象的生命周期中,我们已知,操作执行的前提是要进入Ready状态,而要进入Ready状态,当存在依赖的情况下,还需该操作所有依赖全部执行完成。而并发的情况下可能有多个操作进入Ready状态,那么到底哪一个操作对象优先执行呢?此时,才是由优先级来决定的。

所以操作依赖 影响的是操作是否可以进入Ready状态,操作优先级 影响的是进入Ready状态下的各操作之间的开始执行顺序。

如果一个队列中既包含高优先级操作,又包含低优先级操作,并且两个操作都Ready状态,那么队列先执行高优先级操作。 如果,一个队列中既包含了Ready状态的操作,又包含了未Ready状态的操作,即使未Ready状态的操作优先级比Ready状态的操作优先级高。那么也是会先执行Ready状态的操作。

优先级不能取代依赖关系。优先级只对Ready状态下的操作对象确定执行顺序;先满足依赖关系,然后再根据优先级,从已Ready状态下的所有操作中取优先级最高的那个执行。如果要控制操作间的启动顺序,则必须使用依赖关系。

3.8 服务质量

QoS维护五种优先级,推出于iOS8.0,它的出现统一了Cocoa中所有多线程技术的优先级。正确的使用QoS来指定线程或任务优先级可以使iOS更加智能地分配硬件资源,以提高执行效率和控制电量。

typedef NS_ENUM(NSInteger, NSQualityOfService) {
    NSQualityOfServiceUserInteractive = 0x21,
    NSQualityOfServiceUserInitiated = 0x19,
    NSQualityOfServiceUtility = 0x11,
    NSQualityOfServiceBackground = 0x09,
    NSQualityOfServiceDefault = -1
} API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));

NSOperation对象 的queuePriority属性影响操作在队列内的执行优先级,而qualityOfService属性影响操作在系统级执行的优先级。

根据CPU、网络和磁盘的分配来创建一个操作的系统优先级,一个高质量的服务就意味着更多的资源得以提供来更快地完成操作。

NSOperationQueue对象也维护着一个queuePriority属性。

队列的 QOS 设置,会自动把较低优先级的操作提升到与队列相同优先级。(原更高优先级操作的优先级保持不变)。后续添加进队列的操作,优先级低于队列优先级时,也会被自动提升到与队列相同的优先级。

4. 自定义NSOperation子类

NSOperation类本身是多核的。因此,从多个线程调用NSOperation对象的方法是安全的,而不需要创建额外的锁来同步对该对象的访问。这种行为是必要的,因为操作通常在与创建和监视它的线程分离的线程中运行。
当你子类化NSOperation时,你必须确保任何被覆盖的方法在多线程调用时都是安全的。如果在子类中实现自定义方法(如自定义数据访问器),还必须确保这些方法是线程安全的。因此,必须同步对操作中的任何数据变量的访问,以防止潜在的数据损坏。

自定义NSOperation子类的方式取决于操作是设计为并发执行还是非并发执行

不管是并发执行还是非并发执行,我们都应定义一个自定义的初始化方法,以便更容易创建自定义类的实例,并保存相关数据。

对于非并发执行操作,通常只需重写main方法即可。在这个方法中添加想要实现的功能。

对于并发操作,至少重写四个方法:startasynchronousexecutingfinished。并且需要自己创建自动释放池,因为异步操作无法访问主线程的自动释放池。

注意:在自定义子类时,经常通过cancelled属性检查方法是否取消,并且对取消的做出响应。

4.1 非并发的NSOperation子类

非并发的子类只需要继承NSOperation后,重写main函数,直接将需要执行的任务放在main方法中,然后直接调用即可。当然可能还需要实现初始化方法以配置相关数据和其他与任务相关的代码。

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

如:在main函数伊始或者在执行了一段耗时操作和其他需要考虑的情况。

#import "MyOperation.h"

@implementation MyOperation

- (instancetype)init {
    if (self = [super init]) {
        // 配置数据
    }
    return self;
}

- (void)main {
    if (self.isCancelled) {
        return;
    }
    .......
    // 耗时操作
    if (self.isCancelled) {
        return;
    }
    .......
    // 异步请求
    if (self.isCancelled) {
        return;
    }
    //执行任务
    [self performTask];
}

- (void)performTask {
    for (int i = 0; i < 2; i++) {
        sleep(2);
        NSLog(@"执行任务---%@",[NSThread currentThread]);
    }
}
@end

使用MyOperation:

- (void)useMyOperation {
    MyOperation *op = [[MyOperation alloc] init];
    NSLog(@"begin");
    [op start];
    NSLog(@"end");
}

打印结果如下:

begin
执行任务---<NSThread: 0x600002c4c380>{number = 1, name = main}
执行任务---<NSThread: 0x600002c4c380>{number = 1, name = main}
end

任务同步执行,当start方法返回时,操作进入Finished状态。

4.2 并发的NSOperation子类

实现并发的自定义子类,需要至少重写下面几个方法或属性:

  • start:把需要执行的任务放在start方法里,任务加到队列后,队列会管理任务并在线程被调度后,调用start方法,不需要调用父类的方法(不要 [super start]);
  • asynchronous:表示是否并发执行;
  • executing:表示任务是否正在执行,如果操作正在执行其任务,则返回YES,否则为NO。如果重写了操作对象的start方法,那么还需要手动调用KVO方法来进行通知;
  • finished:表示某个操作已成功完成其任务,或已取消并退出,直到isFinished的值更改为YES,操作对象才会清除依赖项并退出操作队列。 如果重写了start方法,那么还必须替换finished属性,并在操作完成执行或取消时生成KVO通知。

在并发操作中,start方法负责以异步方式启动操作。无论派生一个线程还是调用一个异步函数,都是通过这个方法完成的。在此方法中我们需要改变executing更新状态,发送executing KVO通知。当结束Operation。必须更新isExecutingisFinished状态,并触发KVO通知。当cancel一个Operation时候,我们也要更新isFinished状态,即使此时Operation还未执行完毕。在Queue 中的Operation必须进入cancel状态后才可以被从Operation中移除。

#import "MyOperation.h"

@interface MyOperation ()

@property (nonatomic, getter=isExecuting) BOOL executing;
@property (nonatomic, getter=isFinished)  BOOL finished;
@property (nonatomic, copy)               NSString *num;
@end

@implementation MyOperation

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

- (instancetype)initWithNum:(NSString *)num {
    if (self = [super init]) {
        // 配置数据
        self.num = num;
    }
    return self;
}

#pragma mark - setter -- getter
- (void)setExecuting:(BOOL)executing {
    //调用KVO通知
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    //调用KVO通知
    [self didChangeValueForKey:@"isExecuting"];
}

- (BOOL)isExecuting {
    return _executing;
}

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

- (BOOL)isFinished {
    return _finished;
}

// 返回YES 标识为并发Operation
- (BOOL)isAsynchronous {
    return YES;
}

#pragma mark - start

- (void)start {
    @autoreleasepool{
        self.executing = YES;
        if (self.cancelled) {
            [self done];
            return;
        }
        //执行任务
        [self performTask];
    }
    // 任务执行完成,手动设置状态
    [self done];
}

// 任务完成,修改状态
- (void)done {
    self.finished = YES;
    self.executing = NO;
}

// 执行任务
- (void)performTask {
    for (int i = 0; i < 2; i++) {
        sleep(2);
        NSLog(@"执行任务%@---%@",self.num,[NSThread currentThread]);
    }
}
@end


使用MyOperation:

- (void)useMyOperation {
    NSLog(@"begin");
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    MyOperation *op = [[MyOperation alloc] initWithNum:@"1"];
    MyOperation *op2 = [[MyOperation alloc] initWithNum:@"2"];
    MyOperation *op3 = [[MyOperation alloc] initWithNum:@"3"];
    [queue addOperation:op];
    [queue addOperation:op2];
    [queue addOperation:op3];
    op.completionBlock = ^{
        NSLog(@"finish");
    };
    NSLog(@"end");
}

打印结果如下:

begin
end
执行任务2---<NSThread: 0x60000063e880>{number = 2, name = (null)}
执行任务1---<NSThread: 0x60000063a540>{number = 4, name = (null)}
执行任务3---<NSThread: 0x60000063cb40>{number = 8, name = (null)}
执行任务3---<NSThread: 0x60000063cb40>{number = 8, name = (null)}
执行任务2---<NSThread: 0x60000063e880>{number = 2, name = (null)}
执行任务1---<NSThread: 0x60000063a540>{number = 4, name = (null)}
finish