Cocoa operations
是一种面向对象的方式,用于封装要异步执行的工作。Operation
被设计成可以与operation queue
一起使用,也可以单独使用。
- 由于
Operation
是基于Objective-C
的,所以在MacOS
和iOS
基于Cocoa
的应用程序中最常使用。
NSOperation
在
Foundation
框架中,operation object
是NSOperation
类的一个实例,用于封装你希望应用程序执行的工作,适用于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
,该block
在operations
的主任务完成后执行。支持使用
KVO
通知监听operations
执行状态的变化。支持对
operations
进行优先级排序,从而影响它们相对执行顺序。支持取消语义(
cancel
),允许在执行operation
时暂停操作。
Operations
旨在帮助你提高应用程序中的并发水平。Operations
也是将应用程序的行为组织并封装为简单的离散块 (discrete chunks
) 的好方法。
1 | @interface NSOperation : NSObject |
并发与非并发操作
1、operations
和 operation queue
通常,通过将operations
添加到operation queue
来执行它们,但这样做不是必需的。也可以通过调用operation object
的start
方法手动执行它,但这样做不能保证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 | NSInvocationOperation *oper = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download:) object:@"开始下载"]; |
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 | let operation = BlockOperation { |
自定义NSOperation
如果block operation
和 invocation 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 | @interface ZMOperation () |
1 | ZMOperation *operation = [[ZMOperation alloc] initWithData:@[@"111", @"222", @"333"]]; |
2、Custom concurrent Operations
1 | @interface MyOperation () |
1 | MyOperation *operation = [[MyOperation alloc] init]; |
保持遵循 KVO
NSOperation类的以下key path
(关键路径)遵循KVO
:
1 | isCancelled |
例如:重写 start
时,需要关注 isExecuting
、isFinished
仍然遵循 KVO
要求
1 | operation.addObserver(self, forKeyPath: "isFinished", options: [.new], context: nil) |
自定义Operation的执行行为
operation objects
的配置发生在创建它们后,但在将它们添加到队列(queue
)之前。下面描述的配置类型可以应用于所有operation objects
,无论您是自己将NSOperation
子类化还是使用现有子类。
配置互操作依赖项
Dependencies
(依赖项)是 序列化 不同operation objects
的执行的一种方式。
依赖于其他operation
的operation
在 其依赖 的所有operation
完成执行之前不能开始执行。因此,可以使用依赖关系在两个 operation object
之间创建简单的一对一依赖关系,或者构建复杂的对象依赖关系图(dependency graphs
)。
要在两个operation object
之间建立依赖关系,需要使用 NSOperation
的addDependency:
方法。此方法创建从当前operation object
到指定为参数的目标operation
的单向依赖关系。这种依赖性意味着在目标对象完成执行之前,当前对象无法开始执行。
Operation object
管理它们自己的依赖关系,因此可以在operation
之间创建依赖关系并将它们全部添加到不同的queue
中。注意,不能在 operation
之间创建循环依赖关系,这样做将阻止受影响的operation
永远运行。
当operation
的所有依赖项都已完成执行后,operation object
通常可以执行了。 (如果自定义isReady
方法的行为,则operation
的就绪状态由您设置的条件决定。)如果 operation object
在队列中,则该队列可随时开始执行该操作。如果计划手动执行该operation
,则由您调用该operation
的start
方法。
注意:必须在运行
operation
或将其添加到operation queue
之前配置依赖项,之后添加的依赖项可能不会阻止给定的operation object
运行。
1 | let queue1 = OperationQueue() |
更改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-priority
和low-priority
的operation
,并且两个operation
都准备就绪,则该队列首先执行high-priority operation
。但是,如果high-priority operation
尚未准备就绪,但low-priority operation
已准备就绪,则队列首先执行low-priority operation
。如果要阻止某个operation
在另一operation
完成之前开始,则必须使用依赖项(如配置互操作依赖项中所述)。
1 | let queue = OperationQueue() |
更改基础线程优先级
系统中的线程策略(Thread policies
)本身由内核管理,但是通常,优先级较高的线程比低优先级的线程有更多的运行机会。
在operation object
中,将线程优先级指定为0.0~1.0
的浮点值,其中0.0是最低优先级,而1.0是最高优先级。如果未指定显式线程优先级,则operation
将以默认线程优先级0.5运行。
要设置operation
的线程优先级,则必须先调用operation object
的setThreadPriority:
方法,然后再将其添加到队列(或手动执行)。在执行operation
时,默认的start
方法使用您指定的值来修改当前线程的优先级。此新优先级仅在operation
的main
方法期间有效。所有其他代码(包括operation
的 completion block
)均以默认线程优先级运行。如果创建concurrent operation
,并因此重写了start
方法,则必须自己配置线程优先级。
配置 Completion Block
一个 operation
可以在其主要任务完成执行时执行completion block
,通过NSOperation
的 setCompletionBlock:
方法。
可以使用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 | NSInvocationOperation *selOper = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(operationAction) object:nil]; |
1、创建 operation queue 并发执行 operation
1 | NSOperationQueue *queue = [[NSOperationQueue alloc] init]; |
2、返回与主线程相关的operation queue
- 该 queue 在应用程序的主线程上一次执行一个operation
- 该 queue 在run loop common中执行这些操作
- 队列的underlyingQueue属性的值是主线程的调度队列,不能将此属性设置为其他值。
1 | NSOperationQueue *queue = [NSOperationQueue mainQueue]; |
使用 mainQueue
进行线程间通讯
1 | NSOperationQueue *queue = [[NSOperationQueue alloc] init]; |
3、返回启动当前 operation 的 operation queue
可以在运行的 operation object 中使用此方法来获取对启动它的 operation queue的引用。从正在运行的 operation 的上下文外部调用这个方法通常会返回 nil。
1 | - (void)operationAction { |
Operation 添加到 Operation Queue 的方法
1 | func addOperation(Operation) |
如上方法中的每一个都将一个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
方法启动它。
在operation
的isReady
方法返回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 | func performOperation(_ oper: Operation) -> Bool { |
取消操作
一旦添加到operation queue
,operation object
实际上就归queue
所有,并且无法删除。 让一个operation
退出队列的唯一方法是取消它。可以通过调用单个operation object
的cancel
方法来取消它,也可以通过调用queue object
的cancelAllOperations
方法来取消队列中的所有operation objects
。
只有在确定不再需要operations
时,才应取消operations
。发出 cancel
命令会将operation object
置于"canceled"
状态,这将阻止它运行。 因为取消的operation
仍被认为是“finished”
的,所以依赖于此operations
的对象将收到相应的KVO
通知以清除该依赖关系。 因此,响应于一些重要事件(如应用程序退出或用户特别请求取消)而 取消所有排队的operations
比 选择性地取消operations
更为常见。
等待操作完成
为了获得最佳性能,你应该将operations
设计为尽可能异步,使应用程序在执行operations
时可以自由地做其他工作。如果创建operation
的代码也处理该operation
的结果,则可以使用NSOperation
的waitUntilFinished
方法阻止该代码,直到operation finishes
。 一般来说,如果你可以改进,最好避免调用此方法。 阻塞当前线程可能是一个方便的解决方案,但是它的确在代码中引入了更多的序列化,并限制了总体并发量。
你永远不要等待应用程序主线程中的
operation
,你只应该从辅助线程或其他operation
中这样做。阻塞主线程会阻止应用程序响应用户事件,并可能使应用程序看起来没有响应。
除了等待单个operation
完成之外,还可以通过调用NSOperationQueue
的waitUntilAllOperationsAreFinished
方法来等待队列中的所有操作。当等待整个队列完成时,请注意,应用程序的其他线程仍然可以向队列添加operations
,从而延长等待时间。
挂起和恢复队列
如果要暂时停止operations
的执行,可以使用setSuspended:
方法挂起相应的operation queue
。暂停(或挂起)queue
不会导致已经在执行的operations
在其任务中间暂停。 它只是防止新operations
被安排执行。你可以挂起一个queue
以响应用户暂停任何正在进行的工作的请求,因为期望用户最终可能希望恢复该工作。