Threads(四)Operation Queues
  • Cocoa operations是一种面向对象的方式,用于封装要异步执行的工作。

  • Operation被设计成可以与operation queue一起使用,也可以单独使用。

  • 由于Operation是基于Objective-C的,所以在MacOSiOS基于Cocoa的应用程序中最常使用。

NSOperation

  • Foundation框架中,operation objectNSOperation类的一个实例,用于封装你希望应用程序执行的工作,适用于 iOS 2.0+

  • NSOperation是一个抽象基类,它提供了大量的基础结构,以最大程度地减少你必须在自己的子类中完成的工作量。必须对NSOperation进行子类化才能做任何有用的工作。

  • operation object是一个单发(single-shot)对象,即,它只执行一次任务,而不能再使用它执行一次。

  • Foundation框架提供了两个NSOperation的子类,可以在现有代码中按原样使用。如下表所示:

Class Description
NSInvocationOperation 用于初始化一个operation,这个operation包括调用一个object上的selector。如果你具有已执行所需任务的现有方法,则可以使用此类。此类实现了一个非并发操作。
NSBlockOperation 用于并发的执行一个或多个block对象的类。只有当所有相关block都已完成执行时,operation本身才被视为已完成。
custom NSOperation 子类化NSOperation可以让你完全控制自己操作的实现,包括改变操作执行 和 报告其状态的默认方式的能力。

所有NSOperation对象都支持以下关键功能:

  • 支持在operation对象之间建立 graph-based dependencies(基于图的依赖关系、执行顺序图)。这些dependencies(依赖关系)会阻止给定operations运行,直到它所依赖的所有操作都已运行完毕。

  • 支持可选的completion block,该blockoperations的主任务完成后执行。

  • 支持使用KVO通知监听 operations 执行状态的变化。

  • 支持对operations进行优先级排序,从而影响它们相对执行顺序。

  • 支持取消语义(cancel),允许在执行operation时暂停操作。

Operations旨在帮助你提高应用程序中的并发水平。Operations也是将应用程序的行为组织并封装为简单的离散块 (discrete chunks) 的好方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@interface NSOperation : NSObject

@property (nullable, copy) NSString *name // operation的名称,便于调试时识别它
@property (readonly, getter=isExecuting) BOOL executing; // 表示operation当前是否正在执行
@property (readonly, getter=isFinished) BOOL finished; // 表示operation是否已完成其任务的执行
@property (readonly, getter=isAsynchronous) BOOL asynchronous; // 表示operation是否异步执行其任务。对于相对于当前线程异步运行的操作,此属性的值为YES,对于在当前线程上同步运行的操作,此属性的值为NO。默认值为NO
@property (readonly, getter=isReady) BOOL ready; // 表示是否可以立即执行operation。

// 设置Operation之间的依赖关系。应避免创建任何循环依赖关系,会导致死锁
@property (readonly, copy) NSArray<NSOperation *> *dependencies;
- (void)addDependency:(NSOperation *)op;
- (void)removeDependency:(NSOperation *)op;

- (void)start; // 开始执行操作
- (void)main; // 执行接收方的非并发任务

// 表示Operation是否已取消,默认值为NO。调用cancel方法会将其值置为YES。
// 取消操作不会主动停止接收器的代码执行。操作对象负责定期调用此方法,并在方法返回YES时停止自身。
@property (readonly, getter=isCancelled) BOOL cancelled;
- (void)cancel;

@property (nullable, copy) void (^completionBlock)(void); // Operation执行完后执行的block
// 阻止当前线程的执行,直到操作对象完成其任务为止。
- (void)waitUntilFinished;

@property NSOperationQueuePriority queuePriority; // 操作队列中操作的执行优先级
@property NSQualityOfService qualityOfService // 将系统资源授予操作的相对重要性

并发与非并发操作

1、operationsoperation queue

通常,通过将operations添加到operation queue来执行它们,但这样做不是必需的。也可以通过调用operation objectstart方法手动执行它,但这样做不能保证operation与其余代码同时运行。NSOperation类的isConcurrent方法告诉你operation相对于调用 其start方法的线程而言,是同步运行还是异步运行。默认情况下,该方法返回NO,这意味着operation在调用线程中同步运行。

2、关于 concurrent operation(并发操作)

如果你想实现一个concurrent operation(并发操作),即相对于调用线程异步运行的operation,你必须编写其它的代码来异步启动operation。例如,你可以生成一个单独的线程、调用一个异步系统函数、或者执行任何其他操作,以确保start方法启动任务并立即返回,而且很可能是在任务完成之前返回。

大多数开发人员不需要实现 concurrent operation objects(并发操作对象)。
如果总是将 operation 添加到 operation queue 中,你就不需要实现 concurrent operation(并发操作)。当你将nonconcurrent operation(非并发操作)提交给operation queue(操作队列)时,queue 本身会创建一个线程,在该线程上运行operation。因此,向 operation queue 添加 nonconcurrent operation 仍然会导致operation对象代码的异步执行。只有在需要 异步执行 operation 而不将其添加到 operation queue 的情况下,才需要定义 concurrent operation

NSOperation 子类的基本使用

NSInvocationOperation

NSInvocationOperation对象在运行时,它将调用你在指定对象上指定的 selector。这个类实现了一个non-concurrent operation(非并发操作)。

使用此类可以避免为应用程序中的每个任务定义大量自定义operation objects。特别是如果你正在修改现有应用程序,并且已经拥有执行必要任务所需的对象和方法的情况下。当你要调用的方法可能会根据情况发生变化时,也可以使用它。例如,您可以使用invocation operation 执行根据用户输入动态选择的 selector

NSInvocationOperation 只能用于OC,没有swift对应的类型。

1
2
3
4
5
6
7
8
9
NSInvocationOperation *oper = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download:) object:@"开始下载"];
oper.completionBlock = ^{
NSLog(@"执行完毕, %@", [NSThread currentThread]);
};
[oper start];

// 打印:
// 开始下载, <NSThread: 0x600002dd6900>{number = 1, name = main}
// 执行完毕, <NSThread: 0x6000028d8700>{number = 5, name = (null)}

NSBlockOperation

NSBlockOperation充当一个或多个block对象并发执行的包装器(wrapper)。

此类为已经在使用 operation queues 并且不想同时创建 dispatch queues(调度队列) 的应用程序提供了面向对象的包装器。还可以使用 block operations 来利用dispatch queues(调度队列)中可能不具备的 操作依赖项、KVO通知 和 其他功能。

当你创建一个 block operation 时,通常在初始化时添加至少一个block,稍后根据需要添加更多的block。当需要执行 NSBlockOperation 对象时,该对象将所有 block 提交给默认优先级的并发调度队列(concurrent dispatch queue)。然后,该对象将等待所有 block 执行完成。当最后一个块完成执行时,operation object 将自己标记为finished

因此,您可以使用 block operation 来跟踪一组正在执行的 block,就像使用线程连接来合并来自多个线程的结果一样。不同的是,因为 block operation 本身运行在单独的线程上,所以应用程序的其他线程可以在等待 block operation 完成时继续工作。

如果需要让 NSBlockOperation 串行执行 block 对象,则必须将它们直接提交到所需的分派队列(dispatch queue)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let operation = BlockOperation {
print("init---", Thread.current)
}
operation.addExecutionBlock {
print("add1---", Thread.current)
}
operation.addExecutionBlock {
print("add2---", Thread.current)
}
operation.completionBlock = {
print("completion---", Thread.current)
}
operation.start()

// 打印结果
init--- <NSThread: 0x600002956980>{number = 1, name = main}
add1--- <NSThread: 0x60000297ac80>{number = 4, name = (null)}
add2--- <NSThread: 0x6000029c41c0>{number = 5, name = (null)}
completion--- <NSThread: 0x6000029c41c0>{number = 5, name = (null)}

自定义NSOperation

如果block operationinvocation operation 不能完全满足应用程序的需求,则可以直接将NSOperation子类化,并添加所需的任何行为。 自定义NSOperation需要做的额外工作量,取决于你要实现的是nonconcurrent(非并发)还是concurrent(并发)operation。具体可以查看Apple的示例代码NSOperationSample

定义 nonconcurrent operation (非并发操作)比定义 concurrent operation (并发操作) 简单得多。对于非并发操作,您所要做的就是执行主要任务并适当响应取消事件;现有类基础设施为您完成所有其他工作。对于并发操作,您必须用自定义代码替换一些现有基础设施。

执行主要任务

每个Operation对象至少实现以下方法:

  • 自定义initialization方法,用来将操作对象放入已知状态。
  • main方法,用于执行任务。

也可根据需要实现其它方法,例如:

  • 计划在main 方法中调用的自定义方法
  • 用于设置数据值,访问Operation结果的方法
  • NSCoding协议的方法,对Operation对象归档和解归档

响应取消事件

Operation在执行其任务时,取消操作可能随时发生,尽管NSOperation提供了cancel方法,但是识别取消事件是必要的。如果一个 operation 被直接终止,可能没有办法回收已分配的资源。因此,operation objects 需要检查取消事件,当它们在操作过程中发生时,优雅地退出。

为了让 operation object 支持 cancel 操作,需要在自定义代码中定期的调用 isCancelled 判断是否返回。isCancelled方法本身是非常轻量级的,可以频繁调用,而不会对性能造成很大的影响。

通常,在Custom Operation的代码中以下位置调用 isCancelled 方法:

  • 在你执行任何实际工作之前;
  • 在循环的每次迭代期间至少一次,如果每次迭代相对较长,则更频繁;
  • 在代码中相对容易中止操作的任何位置;

将自定义Operation配置为并发

Operation objects 默认以同步方式执行,也就是说,它们在调用其start方法的线程中执行其任务。由于操作队列为非并发操作提供了线程,因此大多数操作仍然异步运行。

为了实现并发Operation,通常需要重写如下表方法:

方法 描述
start (必需)concurrent operations必须重写此方法,,并用它们自己的自定义实现替换默认行为。要手动执行操作,需要调用其start方法。因此,此方法的实现是操作的起点,也是设置执行任务的线程或其他执行环境的地方。您的实现在任何时候都不能调用super。
main (可选)此方法通常用于实现与操作对象关联的任务。尽管可以在start方法中执行任务,但使用此方法实现任务可以使设置代码和任务代码更清晰地分离。
isExecuting isFinished (必需)并发操作负责设置其执行环境并向外部客户报告该环境的状态。因此,并发操作必须维护一些状态信息,以便知道它何时在执行其任务以及何时完成了该任务。然后它必须使用这些方法报告该状态。

这些方法的实现必须是安全的,才能同时从其他线程调用。更改这些方法报告的值时,还必须为预期的密钥路径生成适当的KVO通知。
isConcurrent (必需)若要将操作标识为并发操作,请重写此方法并返回“是”。

1、Custom nonconcurrent Operations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@interface ZMOperation ()

@property (nonatomic, strong)NSArray <NSString *>*strArray;

@end

@implementation ZMOperation

- (instancetype)initWithData: (NSArray <NSString *>*)arr {
if (self = [super init]) {
self.strArray = arr;
}
return self;
}

// 执行接收方的非并发任务。重写此方法以执行所需的任务。
// 此方法将在NSOperation提供的 autorelease pool 中自动执行,因此您不需要在实现中创建自己的 autorelease pool块。
// 如果要实现并发操作,则不需要重写此方法;但如果计划从自定义start方法调用它,则可以这样做。
- (void)main{

BOOL isDone = NO;
while (![self isCancelled] && !isDone) { // 执行任务前判断是否 取消操作

for (int i = 0; i < self.strArray.count; i++) {

if ([self isCancelled]) { // 每次迭代期间是否 取消操作
return;
}

NSLog(@"%@, %@", self.strArray[i], [NSThread currentThread]);
[NSThread sleepForTimeInterval:2.0];

if (i == self.strArray.count - 1) { // 任务结束后
isDone = true;
}
}
}
}

@end
1
2
3
4
5
6
7
8
9
10
11
12
ZMOperation *operation = [[ZMOperation alloc] initWithData:@[@"111", @"222", @"333"]];
operation.completionBlock = ^{
NSLog(@"completion---%@", [NSThread currentThread]);
};
[operation start];

// 打印结果:
// 111, <NSThread: 0x6000017ea940>{number = 1, name = main}
// 222, <NSThread: 0x6000017ea940>{number = 1, name = main}
// 333, <NSThread: 0x6000017ea940>{number = 1, name = main}
// XPC connection interrupted
// completion---<NSThread: 0x6000017d3d80>{number = 3, name = (null)}

2、Custom concurrent Operations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
@interface MyOperation ()
{
BOOL executing;
BOOL finished;
}
@end

@implementation MyOperation

- (instancetype)init
{
self = [super init];
if (self) {
executing = NO;
finished = NO;
}
return self;
}

- (void)start {
// 任务开始前,检查是否 取消操作
if ([self isCancelled])
{
// operation被取消,将任务置为完成
[self willChangeValueForKey:@"isFinished"];
finished = YES;
[self didChangeValueForKey:@"isFinished"];
return;
}

// operation未被取消,则开始执行任务
[self willChangeValueForKey:@"isExecuting"];
[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
executing = YES;
[self didChangeValueForKey:@"isExecuting"];
}

- (void)main {
@try {
// 开始执行operation
for (int i = 0; i < self.strArray.count; i++) {

if ([self isCancelled]) { // 每次迭代期间是否 取消操作
return;
}

NSLog(@"%@, %@", self.strArray[i], [NSThread currentThread]);
[NSThread sleepForTimeInterval:2.0];
}

[self completeOperation];
}
@catch(...) {

}
}

// 完成操作
- (void)completeOperation {
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];

executing = NO;
finished = YES;

[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}

- (BOOL)isConcurrent {
return true;
}

- (BOOL)isExecuting {
return executing;
}

- (BOOL)isFinished {
return finished;
}

@end
1
2
3
4
5
6
7
8
9
10
11
12
MyOperation *operation = [[MyOperation alloc] init];
operation.strArray = @[@"111", @"222", @"333"];
operation.completionBlock = ^{
NSLog(@"completion---%@", [NSThread currentThread]);
};
[operation start];

// 打印:
// 111, <NSThread: 0x600003f191c0>{number = 5, name = (null)}
// 222, <NSThread: 0x600003f191c0>{number = 5, name = (null)}
// 333, <NSThread: 0x600003f191c0>{number = 5, name = (null)}
// completion---<NSThread: 0x600003f361c0>{number = 3, name = (null)}

保持遵循 KVO

NSOperation类的以下key path(关键路径)遵循KVO

1
2
3
4
5
6
7
8
isCancelled
isConcurrent
isExecuting
isFinished
isReady
dependencies
queuePriority
completionBlock

例如:重写 start 时,需要关注 isExecutingisFinished 仍然遵循 KVO 要求

1
2
3
4
5
operation.addObserver(self, forKeyPath: "isFinished", options: [.new], context: nil)

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
print(keyPath)
}

自定义Operation的执行行为

operation objects的配置发生在创建它们后,但在将它们添加到队列(queue)之前。下面描述的配置类型可以应用于所有operation objects,无论您是自己将NSOperation子类化还是使用现有子类。

配置互操作依赖项

Dependencies(依赖项)是 序列化 不同operation objects的执行的一种方式。

依赖于其他operationoperation 在 其依赖 的所有operation 完成执行之前不能开始执行。因此,可以使用依赖关系在两个 operation object 之间创建简单的一对一依赖关系,或者构建复杂的对象依赖关系图(dependency graphs)。

要在两个operation object之间建立依赖关系,需要使用 NSOperationaddDependency: 方法。此方法创建从当前operation object到指定为参数的目标operation的单向依赖关系。这种依赖性意味着在目标对象完成执行之前,当前对象无法开始执行。

Operation object管理它们自己的依赖关系,因此可以在operation之间创建依赖关系并将它们全部添加到不同的queue 中。注意,不能在 operation 之间创建循环依赖关系,这样做将阻止受影响的operation 永远运行。

operation的所有依赖项都已完成执行后,operation object通常可以执行了。 (如果自定义isReady方法的行为,则operation的就绪状态由您设置的条件决定。)如果 operation object 在队列中,则该队列可随时开始执行该操作。如果计划手动执行该operation,则由您调用该operationstart方法。

注意:必须在运行operation或将其添加到operation queue之前配置依赖项,之后添加的依赖项可能不会阻止给定的 operation object 运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
let queue1 = OperationQueue()
let downOper1 = BlockOperation {
print("downOper1---", Thread.current)
}
let downOper2 = BlockOperation {
print("downOper2---", Thread.current)
}

let queue2 = OperationQueue()
let upOper1 = BlockOperation {
print("upOper1---", Thread.current)
}
let upOper2 = BlockOperation {
print("upOper2---", Thread.current)
}

// 在不同operation之间建立依赖关系
// 执行顺序图为:upOper2 -> upOper1 -> downOper2 - > downOper1
downOper1.addDependency(downOper2)
downOper2.addDependency(upOper1)
upOper1.addDependency(upOper2)

// 把operation分别添加到不同的queue中
queue1.addOperation(downOper1)
queue1.addOperation(downOper2)
queue2.addOperation(upOper1)
queue2.addOperation(upOper2)

// 执行结果:
upOper2--- <NSThread: 0x600000fdc5c0>{number = 5, name = (null)}
upOper1--- <NSThread: 0x600000fc8f40>{number = 6, name = (null)}
downOper2--- <NSThread: 0x600000fc8f40>{number = 6, name = (null)}
downOper1--- <NSThread: 0x600000f3bb40>{number = 7, name = (null)}

更改Operation的执行优先级

对于添加到队列中的operations,执行顺序 首先 由排队操作(queued operations)的就绪状态决定,然后 由它们的相对优先级(relative priority)决定。

  • 是否准备就绪取决于operation对其他operations的依赖性,但是priority level(优先级)是operation对象本身的属性。

  • Priority levels仅适用于同一operation queue中的operation。在不同的队列中的low-priority operations仍然可以在high-priority operations之前执行。

  • Priority levels不能替代依赖关系。

Priority levels确定operation queue开始仅执行当前准备就绪的那些operations的顺序。例如,如果队列同时包含high-prioritylow-priorityoperation,并且两个operation都准备就绪,则该队列首先执行high-priority operation。但是,如果high-priority operation尚未准备就绪,但low-priority operation已准备就绪,则队列首先执行low-priority operation。如果要阻止某个operation在另一operation完成之前开始,则必须使用依赖项(如配置互操作依赖项中所述)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let queue = OperationQueue()
let operation1 = BlockOperation {
print("operation1---", Thread.current)
}
operation1.queuePriority = .low

let operation2 = BlockOperation {
print("operation2---", Thread.current)
}
operation2.queuePriority = .normal

let operation3 = BlockOperation {
print("operation3---", Thread.current)
}
operation3.queuePriority = .high

queue.addOperation(operation1)
queue.addOperation(operation2)
queue.addOperation(operation3)

// 打印结果:
operation3--- <NSThread: 0x600001d1de00>{number = 4, name = (null)}
operation2--- <NSThread: 0x600001d3edc0>{number = 5, name = (null)}
operation1--- <NSThread: 0x600001d3fbc0>{number = 6, name = (null)}

更改基础线程优先级

系统中的线程策略(Thread policies)本身由内核管理,但是通常,优先级较高的线程比低优先级的线程有更多的运行机会。

operation object中,将线程优先级指定为0.0~1.0的浮点值,其中0.0是最低优先级,而1.0是最高优先级。如果未指定显式线程优先级,则operation将以默认线程优先级0.5运行。

要设置operation的线程优先级,则必须先调用operation objectsetThreadPriority:方法,然后再将其添加到队列(或手动执行)。在执行operation时,默认的start方法使用您指定的值来修改当前线程的优先级。此新优先级仅在operationmain方法期间有效。所有其他代码(包括operationcompletion block)均以默认线程优先级运行。如果创建concurrent operation,并因此重写了start方法,则必须自己配置线程优先级。

配置 Completion Block

一个 operation 可以在其主要任务完成执行时执行completion block,通过NSOperationsetCompletionBlock: 方法。

可以使用completion block来执行不属于主要任务的任何工作。 例如,您可以使用此块来通知感兴趣的客户端操作本身已完成。 并发操作对象可以使用此block来生成其最终的KVO通知。

Operation Object实施的技巧

尽管Operation Object很容易实现,但在编写代码时,有几件事您应该注意。以下部分描述了在为Operation Object编写代码时应考虑的因素。

管理 Operation Object 中的内存

以下各节描述了operation object中良好内存管理的关键元素。关于Objective-C程序中内存管理的一般信息,请参见《高级内存管理编程指南》

避免线程存储(Per-Thread Storage)

尽管大多数operations在线程上执行,但对于nonconcurrent operations,该线程通常由operation queue提供。如果operation queue为你提供了线程,你应该认为该线程属于该队列,而不是你您的operation所触及。具体地说,绝不应该将任何数据与 不是自己创建或管理的线程 相关联。

operation queue管理的线程来来往往取决于系统和应用程序的需求。因此,使用per-thread storage(线程的存储)在operation之间传递数据是不可靠的,而且很可能失败。

对于operation object,在任何情况下都不应该使用per-thread storage。初始化operation对象时,应为该对象提供完成其工作所需的一切。因此,operation object本身提供了所需的上下文存储。所有传入和传出的数据都应存储在此处,直到它们可以集成回应用程序中或者不再需要。

根据需要保留对Operation Object的引用

仅仅因为operation object是异步运行的,你不应该假设你可以创建它们而忽略它们。它们仍然只是对象,由你来管理代码所需的对它们的任何引用。如果您需要在operation完成后从中检索结果数据,这一点尤其重要。

你应该始终保留自己对operations的引用的原因是,你以后可能没有机会向队列请求对象。队列尽一切努力尽可能快的调度和执行operation。 在许多情况下,队列在添加后几乎立即开始执行operation。当你自己的代码返回到队列以获取对operation的引用时,,该operation可能已经完成并从队列中删除。

处理错误和异常

因为operations本质上是应用程序中离散的实体(discrete entities),所以它们负责处理出现的任何错误或异常。NSOperation 类提供的默认start方法不会捕获异常。你自己的代码应该总是直接捕获和抑制异常。它还应该检查错误代码,并根据需要通知应用程序的适当部分。如果重写了start方法,则必须以类似的方式捕获自定义实现中的任何异常,以防止它们离开底层线程的作用域。

你应该准备好处理以下几种错误情况:

1、检查和处理UNIX errno-style的错误代码。

2、检查方法和函数返回的显式错误代码。

3、捕获由你自己的代码或其他系统框架抛出的异常。

4、捕获由NSOperation类本身抛出的异常,它在以下情况下抛出异常:

  • operation尚未准备好执行,但调用了它的start方法时
  • operation正在执行或完成时(可能是因为它被取消了),它的start方法被再次调用
  • 尝试向已经执行或已完成的operation添加completion block
  • 当你试图检索被取消的NSInvocationOperation对象的结果时

如果你的自定义代码确实遇到了异常或错误,则应该采取任何必要的步骤将该错误传播到应用程序的其余部分。NSOperation类不提供显式方法来将错误结果代码或异常传递给应用程序的其他部分。因此,如果这些信息对应用程序很重要,则必须提供必要的代码。

确定Operation Objects的适当范围

尽管可以向operation queue中添加任意数量的operation,但这样做通常是不切实际的。与任何对象一样,NSOperation类的实例消耗内存,并具有与其执行相关的实际成本。如果你的每个 operation objects 仅执行少量工作,并且你创建了数以万计的operation objects,那么你可能会发现调度操作(dispatching operations)所花的时间比实际工作要多。而且,如果你的应用程序已经受到内存限制(memory-constrained),你可能会发现仅仅在内存中包含成千上万个operation objects可能会进一步降低性能。

  • 有效使用operations的关键是 在需要做的工作量 和 保持计算机繁忙 之间找到一个适当的平衡。
    尽量确保你的operations完成了合理的工作量。例如,如果你的应用程序创建了100个operation objects以对100个不同的值执行相同的任务,那么请考虑创建10个operation objects来分别处理10个值。

  • 还应该避免一次性向queue中添加大量operations,或者避免将operation objects连续添加到队列中的速度快于处理对象的速度。与其用operation objects充斥队列,不如成批创建这些对象。当一个批处理完成执行时,使用completion block告诉应用程序创建一个新的批处理。当你有很多工作要做时,你希望队列中充满足够的操作,从而使计算机保持忙碌,但您不希望一次创建这么多操作,从而使应用程序耗尽内存。

当然,您创建的operation objects的数量以及在每个对象中执行的工作量是可变的,并且完全取决于您的应用程序。 您应该经常使用Instruments之类的工具来帮助您在效率和速度之间找到适当的平衡。 有关可用于收集代码指标的工具和其他性能工具的概述,请参见性能概述

执行Operations

把 Operations 添加到 Operation Queue 执行

  • 执行operations的最简单方法是使用operation queue,它是NSOperationQueue类的实例。

  • 应用程序负责创建和维护它打算使用的任何operation queues

  • 一个应用程序可以有任意数量的queues,但是在给定的时间点上执行的operations数量是有实际限制的。

  • operation queues与系统协同工作,将 concurrent operations 的数量限制为适合可用内核和系统负载的值。因此,创建额外的queues并不意味着你可以执行额外的operations

1
2
3
4
5
6
7
8
9
10
11
12
13
NSInvocationOperation *selOper = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(operationAction) object:nil];
selOper.completionBlock = ^{
NSLog(@"selOper-completion--: %@", [NSThread currentThread]);
};
NSBlockOperation *blockOper = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"block init--:%@", [NSThread currentThread]);
}];
[blockOper addExecutionBlock:^{
NSLog(@"block add--:%@", [NSThread currentThread]);
}];
blockOper.completionBlock = ^{
NSLog(@"blockOper-completion--: %@", [NSThread currentThread]);
};

1、创建 operation queue 并发执行 operation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:selOper];
[queue addOperation:blockOper];

- (void)operationAction {
NSLog(@"target-selector--: %@", [NSThread currentThread]);
}

// 打印:
// target-selector--: <NSThread: 0x6000006c0340>{number = 3, name = (null)}
// block init--:<NSThread: 0x6000006e5100>{number = 5, name = (null)}
// block add--:<NSThread: 0x6000006e0080>{number = 6, name = (null)}
// blockOper-completion--: <NSThread: 0x6000006c0340>{number = 3, name = (null)}
// selOper-completion--: <NSThread: 0x6000006e0080>{number = 6, name = (null)}

2、返回与主线程相关的operation queue

  • 该 queue 在应用程序的主线程上一次执行一个operation
  • 该 queue 在run loop common中执行这些操作
  • 队列的underlyingQueue属性的值是主线程的调度队列,不能将此属性设置为其他值。
1
2
3
4
5
6
7
8
9
10
NSOperationQueue *queue = [NSOperationQueue mainQueue];
[queue addOperation:selOper];
[queue addOperation:blockOper];

// 打印:
// target-selector--: <NSThread: 0x600002632940>{number = 1, name = main}
// selOper-completion--: <NSThread: 0x60000267f980>{number = 3, name = (null)}
// block init--:<NSThread: 0x600002632940>{number = 1, name = main}
// block add--:<NSThread: 0x600002663e00>{number = 5, name = (null)}
// blockOper-completion--: <NSThread: 0x600002663e00>{number = 5, name = (null)}

使用 mainQueue 进行线程间通讯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

NSBlockOperation *blockOper = [NSBlockOperation blockOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:@"http:2.jpg"];
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];

// 回到主线程刷新UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.imgView.image = image;
NSLog(@"%@", [NSThread currentThread]);
}];

}];

[queue addOperation:blockOper];

3、返回启动当前 operation 的 operation queue

可以在运行的 operation object 中使用此方法来获取对启动它的 operation queue的引用。从正在运行的 operation 的上下文外部调用这个方法通常会返回 nil。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- (void)operationAction {
NSLog(@"target-selector--: %@", [NSThread currentThread]);

// 获取当前 operation queue 添加 operation
NSInvocationOperation *oper = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(otherOperationAction) object:nil];

NSOperationQueue *queue = [NSOperationQueue currentQueue];
[queue addOperation:oper];
}

- (void)otherOperationAction {
NSLog(@"other target-selector--: %@", [NSThread currentThread]);
}

// 执行
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:selOper];
[queue addOperation:blockOper];

// 打印:
// block add--:<NSThread: 0x6000001f6540>{number = 5, name = (null)}
// target-selector--: <NSThread: 0x6000001bb240>{number = 3, name = (null)}
// block init--:<NSThread: 0x6000001bae00>{number = 4, name = (null)}
// blockOper-completion--: <NSThread: 0x6000001f6540>{number = 5, name = (null)}
// selOper-completion--: <NSThread: 0x6000001bb240>{number = 3, name = (null)}
// other target-selector--: <NSThread: 0x6000001bae00>{number = 4, name = (null)}

Operation 添加到 Operation Queue 的方法

1
2
3
func addOperation(Operation)
func addOperations([Operation], waitUntilFinished: Bool)
func addOperation(() -> Void)

如上方法中的每一个都将一个operation(或多个operation)排队,并通知队列它应该开始处理它们。 在大多数情况下,operations在添加到queue后不久执行,但是operation queue可能会由于以下任何原因而延迟排队操作的执行。 具体来说,如果排队的操作(queued operations)依赖于尚未完成的其他operations,则执行可能会延迟。 如果operation queue本身已挂起或已经在执行其最大数量的并发操作,则执行也可能会延迟。

注意:
在将operation object添加到queue之前,应该对operation object进行所有必要的配置和修改,因为一旦添加了operation object,该operation就可以在任何时间运行,对于进行更改以达到预期的效果而言可能为时已晚。

尽管NSOperationQueue类是为并发执行操作而设计的,但是可以强制单个队列一次仅运行一个operation。使用setMaxConcurrentOperationCount:方法可以配置operation queue object的最大并发操作数。将值1传递给此方法将导致队列一次仅执行一个operation。尽管一次只能执行一个operation,但是执行的顺序仍然基于其他因素,例如每个操作的就绪性及其分配的优先级。因此,序列化operation queue提供的行为与Grand Central Dispatch中的 串行调度队列(serial dispatch queue) 所提供的行为完全不同。如果operation objects的执行顺序对您很重要,则应在将operations添加到queue之前使用依赖关系来建立顺序。

手动执行 Operations

尽管operation queues是运行operation object的最方便的方法,但是也可以在没有 queue 的情况下执行 operations。 但是,如果选择手动执行operations,则应在代码中采取一些预防措施。 特别是,operation必须准备就绪可以运行,并且必须始终使用其start方法启动它。

operationisReady方法返回YES之前,该operation被认为无法运行行。 isReady方法已集成到NSOperation类的依赖管理系统(dependency management system)中,以提供operation依赖关系的状态。 只有清除了其依赖性后,operation才可以自由地开始执行操作。

手动执行operation时,应始终使用start方法开始执行。你使用此方法,而不是main方法或其他方法,因为start方法在实际运行自定义代码之前会执行多项安全检查。 特别是,默认的start方法将生成KVO通知,operations需要这个通知才能正确处理它们的依赖关系。 如果该operation已被取消,则此方法还可以正确地避免执行该operation,如果你的operation实际上尚未准备好运行,则该方法将引发异常。

如果你的应用程序定义了并发操作对象(concurrent operation objects),则还应该在启动它们之前考虑调用isConcurrent操作方法。 如果此方法返回NO,则您的本地代码可以决定是在当前线程中同步执行operation,还是首先创建单独的线程。 但是,实施这种检查完全取决于你。

如下手动执行 Operation 前需要做的检查,如果方法返回false,则可以安排Timer稍后再执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func performOperation(_ oper: Operation) -> Bool {
var res = false
if oper.isReady && oper.isCancelled == false {
if oper.isConcurrent == false {
oper.start()
} else {
Thread.detachNewThread {
oper.start()
}
}
res = true
} else if oper.isCancelled {

self.willChangeValue(forKey: "isExecuting")
self.willChangeValue(forKey: "isFinished")
executing = false
finished = true
self.didChangeValue(forKey: "isExecuting")
self.didChangeValue(forKey: "isFinished")

res = true
}
return res
}

取消操作

一旦添加到operation queueoperation object实际上就归queue所有,并且无法删除。 让一个operation退出队列的唯一方法是取消它。可以通过调用单个operation objectcancel方法来取消它,也可以通过调用queue objectcancelAllOperations方法来取消队列中的所有operation objects

只有在确定不再需要operations时,才应取消operations。发出 cancel 命令会将operation object置于"canceled"状态,这将阻止它运行。 因为取消的operation仍被认为是“finished”的,所以依赖于此operations的对象将收到相应的KVO通知以清除该依赖关系。 因此,响应于一些重要事件(如应用程序退出或用户特别请求取消)而 取消所有排队的operations 比 选择性地取消operations 更为常见。

等待操作完成

为了获得最佳性能,你应该将operations设计为尽可能异步,使应用程序在执行operations时可以自由地做其他工作。如果创建operation的代码也处理该operation的结果,则可以使用NSOperationwaitUntilFinished方法阻止该代码,直到operation finishes。 一般来说,如果你可以改进,最好避免调用此方法。 阻塞当前线程可能是一个方便的解决方案,但是它的确在代码中引入了更多的序列化,并限制了总体并发量。

你永远不要等待应用程序主线程中的operation,你只应该从辅助线程或其他operation中这样做。阻塞主线程会阻止应用程序响应用户事件,并可能使应用程序看起来没有响应。

除了等待单个operation完成之外,还可以通过调用NSOperationQueuewaitUntilAllOperationsAreFinished方法来等待队列中的所有操作。当等待整个队列完成时,请注意,应用程序的其他线程仍然可以向队列添加operations,从而延长等待时间。

挂起和恢复队列

如果要暂时停止operations的执行,可以使用setSuspended:方法挂起相应的operation queue。暂停(或挂起)queue不会导致已经在执行的operations在其任务中间暂停。 它只是防止新operations被安排执行。你可以挂起一个queue以响应用户暂停任何正在进行的工作的请求,因为期望用户最终可能希望恢复该工作。

学习博客

Operation Queues

NSBlockOperation 面试与正确用法

文章作者: Czm
文章链接: http://yoursite.com/2020/10/20/Threads-%E5%9B%9B-Operation-Queues/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Czm