NSOperation、NSOperationQueue

2019-06-11  本文已影响0人  阿斯兰iOS

内容主要是文档的翻译、API 的解释,包括 NSOperation、NSInvocationOperation 、NSBlockOperation 、NSOperationQueue。

官方文档:https://developer.apple.com/reference/foundation/nsoperation

关于 kvo,这里是我写的两篇文章:
NSKey​Value​Observing
Key-Value Observing Programming Guide


一、NSOperation

NSOperation 是个抽象类,不能直接使用,可以用它的子类 NSInvocationOperation 和 NSBlockOperation。

注意,一个操作对象只能执行一次。

一般是把操作对象添加到操作队列(NSOperationQueue 对象),队列会直接在子线程执行操作,或间接通过 GCD 来执行。
也可以不通过队列,而是调用操作的 start 方法来执行。要注意的是,如果操作不处于 ready 状态,会抛异常。

设置 Operation Dependencies 可以定制执行顺序。通过 addDependency: 和 removeDependency: 来添加和删除依赖。所有依赖的操作完成后,操作才会处于 ready 状态。
操作被取消也算是完成。

NSOperation 支持 KVO 和 KVC

  • isCancelled - read-only
  • isAsynchronous - read-only
  • isExecuting - read-only
  • isFinished - read-only
  • isReady - read-only
  • dependencies - read-only
  • queuePriority - readable and writable
  • completionBlock - readable and writable

NSOperation 是线程安全的。
自定义 NSOperation 的子类,新加的或重写的方法,要自己保证是线程安全的。
When you subclass NSOperation, you must make sure that any overridden methods remain safe to call from multiple threads. If you implement custom methods in your subclass, such as custom data accessors, you must also make sure those methods are thread-safe. Thus, access to any data variables in the operation must be synchronized to prevent potential data corruption. For more information about synchronization, see Threading Programming Guide.

如果操作不添加到队列,而是手动执行,你可以把它设计为异步操作。
操作默认是同步执行的。调用 start 后,会在当前线程执行,操作完成后才返回。
对于异步操作,调用 start 后,会在子线程执行,可能在操作完成之前就返回了。操作可以通过创建新的线程、通过 GCD、或调用异步函数来实现异步执行。设计异步操作,需要自己跟踪和修改操作的状态,并手动发送 KVO 通知。异步操作的用处是不会阻塞当前线程。
如果是通过添加到队列来执行操作,就没必要设计为异步操作了。因为队列总是会在子线程调用操作的 start 方法。

自定义同步操作,一般是重写 main 方法,在里面实现需要完成的任务。如果添加和重写方法,一定要保证线程安全。

自定义异步操作,最少要重写下面的属性和方法:

在 start 方法里面,开启新的线程或调用异步函数来开启任务。
开始后,要更新 executing 属性的值,并手动发送 KVO 通知,key path 是 executing。Your executing property 必须保证是线程安全的。
一旦完成或取消操作,要更新 executing 和 finished 属性的值,还要手动发送 KVO 通知,key path 是 isExecuting and isFinished (generate KVO notifications for both the isExecuting and isFinished key paths)。取消操作也要更新 isFinished 的值,因为只有完成了才能从队列移除。
重写的 executing 和 finished 属性,要保证线程安全。
For additional information and guidance on how to define concurrent operations, see Concurrency Programming Guide.

重要
重写 start 方法不要调用 super。要实现类似 super 的功能,比如开启任务,发送 KVO 通知,在真正开始执行任务前要检查是否被取消了。
For more information about cancellation semantics, see Responding to the Cancel Command.

如果被取消了,正在执行的操作不会立即停止,未执行的操作依然会被调用 start 方法,只不过会立即返回,不会执行真正的任务。并且会忽略依赖操作、被标记为完成,然后被移出队列。一旦取消,自定义的操作要修改 finished to YES and executing to NO。

文档太啰嗦了,总结一下如何设计异步操作。
如果操作不添加到队列,又不想阻塞当前线程,就设计为异步的。重写 start 方法,在里面开启新的线程或调用异步函数执行任务,同时修改 executing 和 finished 属性,要发送 KVO 通知,key path 是 isExecuting and isFinished ,要保证线程安全,在开始执行任务前要判断是否被取消了,不要调用 super。还要重写 asynchronous 属性。文档没说,如果未 ready,start 方法要怎么处理呢。

文档终于说完了,下面开始解释 API。

1.1、初始化

// 子类重写必须调用 super
- (id)init

1.2、执行

1.2.1 start

// 在当前线程开始执行操作
- (void)start;

这个方法会修改操作的状态,调用操作的 main 方法;
会检查操作能否执行,比如已经 cancel 或 finish 的操作,就不会调用它的 main 方法;
正在执行或者还没 ready 的操作,调用 start 方法会抛 NSInvalidArgumentException 异常;
注意,如果操作依赖的操作还没 finish,就认为是 not ready 状态。

如果要并发执行操作,就要重写 start 方法。重写不能调用 super。重写要跟踪和修改操作的状态。当操作开始执行并完成后,要修改 isExecuting 和 isFinished 属性,手动发送 KVO 通知。关于手动 KVO,see Key-Value Observing Programming Guide

你可以手动调用 start 方法。错误的做法是,手动调用 start 方法,又把操作添加到操作队列,或者对操作队列里的操作调用 start 方法。

1.2.2 main

// 非并发的执行操作的任务。
// Performs the receiver’s non-concurrent task.
- (void)main;

main 方法默认什么都不做,你可以重写来执行想要的任务。重写不要调用 super。
main 方法会在操作的自动释放池中执行,所以不需要自己创建自动释放池。
如果想要并发操作,不要重写 main 方法,而是重写 start 方法。

总的来说,要实现自定义任务,可以重写 start 方法或 main 方法。如果要并发执行,就重写 start 方法。

1.2.3 completionBlock

// The block to execute after the operation’s main task is completed.
// 操作被取消,或完成,都会执行。
@property(copy) void (^completionBlock)(void);

completionBlock 一般会在子线程执行,而不是在当前线程执行。可以在 block 里面转移到特定的线程来执行。

当 finished 属性被修改为 YES 的时候会执行 completionBlock。
注意,操作被取消,或者是顺利完成,都会标记为 finished。

iOS 8 之后,this property is set to nil after the completion block begins executing。

1.3、取消

// Advises the operation object that it should stop executing its task.
// 取消尚未执行的操作。
- (void)cancel;

cancel 不会强制停止操作。对已经 finish 的操作没有影响。cancel 会修改操作的内部标记,并尽快从操作队列中移除。

For more information on what you must do in your operation objects to support cancellation, see Responding to the Cancel Command.

1.4、操作状态

1.4.1 cancelled

// 是否被取消。
@property(readonly, getter=isCancelled) BOOL cancelled;

调用 cancel 方法,cancelled 会被设置为 YES。一旦取消,操作会标记为 finished。

取消操作并不能停止它的代码执行。你可以在自定义的 main 方法的开始或过程中检查 cancelled 属性,以便尽快退出操作。

1.4.2 executing

// 是否正在执行。
@property(readonly, getter=isExecuting) BOOL executing;

如果你自定义了并发操作对象,就要重写 executing 方法。重写的话,isExecuting 发生变化时,要手动发送 key path 为 isExecuting 的 KVO 通知。关于手动 KVO,see Key-Value Observing Programming Guide.

You do not need to reimplement this property for nonconcurrent operations.

1.4.3 finished

// 取消 或 完成都是 YES。
@property(readonly, getter=isFinished) BOOL finished;

The value of this property is YES if the operation has finished its main task or NO if it is executing that task or has not yet started it.

如果你自定义了并发操作对象,就要重写 executing 方法。重写的话,isExecuting 发生变化时,要手动发送 key path 为 isFinished 的 KVO 通知。关于手动 KVO,see Key-Value Observing Programming Guide.

You do not need to reimplement this property for nonconcurrent operations.

1.4.4 concurrent

// 是否异步执行。
// 即将废弃,用 asynchronous 代替。
@property(readonly, getter=isConcurrent) BOOL concurrent;

相对于当前线程,是同步还是异步执行。默认值是 NO。

1.4.5 asynchronous

// 是否异步执行。
@property(readonly, getter=isAsynchronous) BOOL asynchronous;

相对于当前线程,是同步还是异步执行。默认值是 NO。

When implementing an asynchronous operation object, you must implement this property and return YES. For more information about how to implement an asynchronous operation, see Asynchronous Versus Synchronous Operations.

1.4.6 ready

// 表示操作现在能否执行。
@property(readonly, getter=isReady) BOOL ready;

由依赖的操作,和自定义的条件决定。
如果想自定义条件,就重写 ready 方法:先调用 super 获取 isReady 的值,再结合自己的条件,返回 YES 或 NO。修改 isReady 后要以 isReady 为 key path 手动发送 KVO 通知。

1.4.7 name

// 操作名字,方便 debug
@property(copy) NSString *name;

1.5、依赖

通过添加依赖可以控制操作的执行顺序。一个操作的所有依赖的操作完成后,它才处于 ready 状态。

1.5.1
// 添加依赖。同样的操作不要添加多次。
// 依赖的操作未完成,会处于 not ready 状态。
// 如果操作正在执行,则添加依赖无效。
// 可能会修改 isReady 的值。
// 添加互相循环依赖会导致死锁。
- (void)addDependency:(NSOperation *)op;
1.5.2
// 删除依赖。
// 可能会修改 isReady 的值。
- (void)removeDependency:(NSOperation *)op;
1.5.3
// 所有的依赖操作。
// 已完成的依赖操作不会从数组中移除。
// 从数组移除的唯一办法是调用 removeDependency: 方法。
@property(readonly, copy) NSArray<NSOperation *> *dependencies;

1.6、操作优先级

1.6.1
// 已废弃,使用 qualityOfService 代替。
@property double threadPriority;
1.6.2
// 竞争系统资源的优先级
@property NSQualityOfService qualityOfService;

// 优先级定义
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));

// 用于 UI 交互,比如处理控件事件和屏幕绘制
NSQualityOfServiceUserInteractive

// 用于用户发起的请求,而且结果必须立即反馈给用户继续交互的任务。
// 比如用户点击列表触发的数据加载。
NSQualityOfServiceUserInitiated

// 用于用户不需要立即得到结果的任务。
// 比如大量文件操作,media 导入等。
NSQualityOfServiceUtility

// 用于用户不可见、无需用户察觉的任务。
// 比如数据备份、内容预加载。
NSQualityOfServiceBackground

// 表示没有定义优先级。
NSQualityOfServiceDefault
1.6.3
// 在操作队列中的优先级,影响各个操作的执行顺序。
// 默认值是 NSOperationQueuePriorityNormal。随便设置的值,会调整为最接近的常量。
// 比如 -10 会调整为 NSOperationQueuePriorityVeryLow。
// queuePriority 不会影响依赖的顺序。
@property NSOperationQueuePriority queuePriority;
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};

1.7、等待

// 阻塞当前线程,直到操作完成。
- (void)waitUntilFinished;

不要在操作的内部调用它,要避免 calling it on any operations submitted to the same operation queue as itself,会死锁。

It is generally safe to call this method on an operation that is in a different operation queue, although it is still possible to create deadlocks if each operation waits on the other.

一般是在创建操作的地方,添加到操作队列后,再调用它。

关于死锁,比如创建操作 a 和操作 b,操作 b 中调用 a 的这个方法,然后先添加 b 到操作队列,再添加 a 到队列。如果操作队列的最大并发数是 1,那就会发生死锁。如果操作队列的最大并发数 > 1 呢,也会死锁吗?


二、NSBlockOperation

NSOperation 的子类。管理并发执行的一个或多个 block。只有所有的 block 执行完了才是 finished 状态。

The blocks themselves should not make any assumptions about the configuration of their execution environment.(啥意思🙃)

For more information about blocks, see Blocks Programming Topics.

// 创建操作。block 不一定在当前线程执行。
+ (instancetype)blockOperationWithBlock:(void (^)(void))block;
// 添加 block 到操作的 block 列表。
// 添加的 block 不一定在当前线程执行,而且是并发执行的。
// 注意,如果操作正在 executing 或者已经 finished,会抛异常。
- (void)addExecutionBlock:(void (^)(void))block;
// 操作的关联 block 列表。
@property(readonly, copy) NSArray<void (^)(void)> *executionBlocks;

三、NSInvocationOperation

NSOperation 的子类。This class implements a non-concurrent operation. 意思是调用 start 方法后,会在当前线程执行。

3.1、创建

// 创建操作。如果 target 没有实现 sel,会返回 nil。
// sel:最多有 1 个类型为 id 的形参。
// sel:返回类型可以是 void,a scalar value,或 id 类型。
- (instancetype)initWithTarget:(id)target selector:(SEL)sel object:(id)arg;

// 指定的初始化方法。
- (instancetype)initWithInvocation:(NSInvocation *)inv;

如果 sel 有返回值,可以在操作完成后调用 result 方法获取返回值。

3.2、获取属性

// 获取 invocation。
@property(readonly, retain) NSInvocation *invocation;
// invocation 或 selector 的返回值。
// 如果操作未完成,值为 nil。
// 如果返回值不是对象,会用 NSValue 包装。
// 如果 method or invocation 发生了异常,调用 result 会发生同样的异常。
// 如果操作取消了,或返回类型是 void 的,调用 result 会发生异常
@property(readonly, retain) id result;
调用 NSInvocationOperation 的 result 方法可能发生的异常。

// invocation method 的返回类型是 void。
const NSExceptionName NSInvocationOperationVoidResultException;

// 操作已经被取消了。
const NSExceptionName NSInvocationOperationCancelledException;

四、NSOperationQueue

For more information about using operation queues, see Concurrency Programming Guide.

注意
队列中的操作,只有 finished 后才会移出队列。
暂停队列不会移除操作,有可能会导致内存泄露。
Operation queues retain operations until they're finished, and queues themselves are retained until all operations are finished. Suspending an operation queue with operations that aren't finished can result in a memory leak.

默认按先进先出的顺序执行,优先级、就绪状态、操作依赖,都会影响执行顺序。
要保证执行顺序,可以使用依赖,即使依赖的操作是在不同的操作队列。
所有依赖的操作完成后,操作才会处于 ready 状态。

取消或完成,都会变成 finished 状态。取消后,操作依然留在队列,但它会尽快结束工作。
对于正在执行的操作,会随时检查自己的 cancel 属性,停止工作并标记自己为 finished 状态。
对于等待执行的操作,队列依然会调用操作的 start 方法,让它处理取消事件和标记自己为 finished 状态。

注意
取消操作会导致它忽略依赖的操作,使得队列可以尽快的调用操作的 start 方法,然后变为 finished 状态,被移出队列。

NSOperationQueue 支持 KVO 和 KVC

  • operations - read-only
  • operationCount - read-only
  • maxConcurrentOperationCount - readable and writable
  • suspended - readable and writable
  • name - readable and writable

NSOperationQueue 是线程安全的。
It is safe to use a single NSOperationQueue object from multiple threads without creating additional locks to synchronize access to that object.
Operation queues use the Dispatch framework to initiate the execution of their operations. As a result, operations are always executed on a separate thread, regardless of whether they are designated as synchronous or asynchronous.

4.1、获取特定队列

// 获取主队列,所有的操作都在主线程的 NSRunLoopCommonModes 执行。
@property(class, readonly, strong) NSOperationQueue *mainQueue;
// 获取当前队列。在操作外部调用可能返回 nil。
@property(class, readonly, strong) NSOperationQueue *currentQueue;

4.2、管理操作

4.2.1 添加操作

// 添加操作。操作完成后会移出队列。
// 一个操作只能同时添加到一个队列,否则会抛异常。
// 添加正在执行或已完成的操作,也会异常。
- (void)addOperation:(NSOperation *)op;
// wait:YES 会阻塞当前线程。
- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait;
- (void)addOperationWithBlock:(void (^)(void))block;

4.2.2 获取操作

// 队列当前的所有正在执行或等待执行的操作。
// 操作在 finish 的时候会移出队列。
// 可以使用 KVO 观察。
@property(readonly, copy) NSArray<__kindof NSOperation *> *operations;
// 当前队列的操作数。
// 队列中的操作完成的时候,operationCount 会发生变化。
// 不要用它来遍历集合或做精确计算。
// 可以使用 KVO 来监听它的变化。
@property(readonly) NSUInteger operationCount;

4.2.3 取消操作

// 取消队列中的所有操作。
- (void)cancelAllOperations;

取消操作不会自动移出队列,不会停止正在执行的操作;
对于正在执行的操作,会随时检查自己的 cancel 属性,停止工作并标记自己为 finished 状态;
对于等待执行的操作,队列依然会调用操作的 start 方法,让它处理取消事件和标记自己为 finished 状态;
取消或完成的操作,在移出队列前,会执行它的 completion block。

4.2.4 同步

// 阻塞当前线程,直到所有操作完成。
// 如果队列为空,函数会立即返回。
- (void)waitUntilAllOperationsAreFinished;

4.3 管理操作的执行

// 竞争系统资源的优先级。比如 CPU 时间,网络、硬盘资源等。
// 如果操作有显式定义,就用操作的值。
// 用户创建的队列,默认值是 NSOperationQualityOfServiceBackground。
// 主队列的默认值是 NSOperationQualityOfServiceUserInteractive。
// 具体定义看 NSOperation 的 qualityOfService。
@property NSQualityOfService qualityOfService;
// 最大并发数
// 修改它对正在执行的操作没有影响。
// 默认值是 NSOperationQueueDefaultMaxConcurrentOperationCount。
// 对于默认值,系统会根据情况设置最大的并发数。
// 设置为 1 就是串行队列了。
@property NSInteger maxConcurrentOperationCount;
// 是否暂停。
// 修改它的值对正在执行的操作没有影响。
@property(getter=isSuspended) BOOL suspended;

4.4 队列属性

// 队列名字
@property(copy) NSString *name;
// GCD 队列。默认是 nil。
// 可以给它设置一个 GCD 队列。
// 设置 GCD 队列时,operationCount 必须为 0,否则抛异常。
// 不能给它设置主线程队列 dispatch_queue_main_t。
// This property automatically retains its assigned queue if OS_OBJECT_IS_OBJC is YES.
@property(assign) dispatch_queue_t underlyingQueue;

欢迎交流。

上一篇下一篇

猜你喜欢

热点阅读