NSOperation是一个抽象类,代表与单个任务相关联的代码和数据。
由于NSOperation是一个抽象类,所以不直接使用它而是使用它的子类或者系统定义的子类(NSInvocationOperation或者NSBlockOperation)来执行任务,尽管是一个抽象类,NSOperation的基本实现包含了重要的逻辑来协调任务安全的执行。这种内置的逻辑使得我们可以专注于任务的具体实现。
operation对象是一次性对象,也就是说它只执行一次任务,不能重复使用。通常把operation添加到一个 操作队列(NSOperationQueue 类的实例)来执行。 操作队列 可以在子线程中直接执行操作,或者使用GCD间接的执行。
如果不想使用操作队列,我们可以直接在代码中调用operation的start
方法来执行操作。 手动执行操作确实给代码带来更多的负担。因为启动一个未ready状态的操作会引发异常。ready
属性指示操作的准备情况
操作依赖性
依赖性是以特定顺序执行操作的便捷方式。 可以使用- (void)addDependency:(NSOperation *)op;
和- (void)removeDependency:(NSOperation *)op;
方法来为操作添加或移除依赖。默认情况下,operation对象直到它依赖的所有的operation对象都执行完毕之后才会被视为准备就绪。一旦最后依赖的操作完成,该operation对象就准备就绪并能够执行
NSOperation的依赖关系不区分关联操作是否成功finished。(换句话说,取消某个操作也会被标记为finished)。在其依赖操作被取消或未成功完成任务的情况下,是否继续执行具有依赖关系的操作取决于我们自己。这可能需要将一些额外的错误跟踪功能添加到operation对象
KVO 兼容属性
NSOperation类的几个属性是支持KVC和KVO的,根据需要,我们可以监测这些属性来控制应用程序的其他部分。使用一下路径监测属性:
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
虽然我们可以观察这些属性,但是不应该将这些属性跟UI元素绑定,因为通常情况下UI元素的操作只能在主线程,而KVO通知可能在任何线程中。
如果为任何上述属性提供自定义实现,则自定义实现必须支持KVC和KVO。 如果自定义的NSOperation对象定义了其他属性,建议也使这些属性支持KVC和KVO。
多核考虑
NSOperation类本身是多核的。因此多线程访问是安全的,不需要创建额外的锁来同步访问。之所以这么设计是因为operation通常在创建并正在监视它的单独线程中运行。
当子类化NSOperation类时,必须确保重写的方法是线程安全的。如果在子类中实现了自定义的方法,必须确定这些方法是线程安全的。因此,必须同步访问operation的任何数据变量,以防止潜在的数据损坏。
异步和同步操作
如果要手动执行operation,而不是将其添加到一个队列中。则可以将operation设计为是同步或异步执行。 operation对象默认是同步的,在同步操作中,operation对象不会创建一个单独的线程来执行任务。当你调用同步操作的start
方法时,该操作会在当前线程立即执行。当对象的start
方法将控制权交还给调用者时,它本身的任务就完成了
当调用异步操作的start
方法时,该方法可能会在其相应的任务完成之前返回。异步操作对象负责在单独的线程上调度它的任务。该操作可以通过直接开启新的线程,调用异步方法,或将block提交到调度队列执行来实现。当控制权返回给调用者时,操作是否正在进行并不重要,只是它可能正在运行。
如果使用队列来执行operation,使用同步操作更简单。如果手动执行操作,则可能需要将操作定义为异步。 定义一个异步操作需要更多工作,因为必须监测任务的持续状态,并且使用KVO通知监测该状态下的更改。但是,如果想要确保手动执行的操作不会阻塞线程,异步操作将会很有用。
当添加一个操作到操作队列中,队列会忽略操作的asynchronous
属性的值, 并始终从一个单独的线程调用start
方法。 因此,如果将operation添加到操作队列中来执行的话,就没有理由使这些operation异步。
子类化说明
NSOperation类提供了追踪操作执行状态的基本逻辑,但是除此之外,必须子类化来完成实际工作。如何创建子类取决于操作是并发执行还是非并发执行。
重写的方法
对于非并发操作,通常只需要重写一个方法:
- (void)main;
在这个方法中,放置执行给定任务所需的代码。当然,也可以自定义初始化方法,来方便的创建实例。我们可能还需要定义一些getter和setter方法来访问operation中的数据,但是,如果定义了自定义的getter/setter方法,必须保证这些方法是线程安全的
如果创建并发操作,则需要至少覆盖以下方法和属性
- (void)start;
@property(readonly, getter=isAsynchronous) BOOL asynchronous;
@property(readonly, getter=isExecuting) BOOL executing;
@property(readonly, getter=isFinished) BOOL finished;
在并发操作中,start
方法负责以异步方式启动操作。无论是生成一个线程还是调用一个异步方法,都可以通过该方法来完成。开始操作时,start
方法应该更新executing
属性(表示操作的执行状态)。可以通过发送KVO通知,让外部知道操作正在运行。executing
属性必须是线程安全的。
完成或取消任务后,并发操作对象必须为isExecuting
和isFinished
路径生成KVO通知,标记操作状态的最终更改。(在取消的情况下,即使操作没有结束它的任务,也要更新isFinished
。 排队的操作在从队列中删除之前必须是已经finished)。除了生成KVO通知,重写的executing
和finished
属性也应该根据操作状态作出相应的更改。
重点:
在自定义的
start
方法中,任何时候都不应该调用super。当定义一个并发操作,需要自己提供跟默认start
方法相同的行为,其中包括启动任务并生成合适的KVO通知。自定义的start
方法也应该检查在实际启动任务之前操作本身是否已经被取消。
即使是并发队列,也很少需要重写上述描述以外的方法。 然而,如果你自定义operation 的依赖功能,则还必须重写其他的方法和提供KVO通知。在有依赖关系的情况下,可能只需要提供isReady
路径的通知。 因为dependencies
属性包含了依赖操作的列表,对它的更改已由默认的NSOperation
类处理
维护operation对象状态
operation对象在内部维护状态信息以确定什么时候执行操作是安全的,并通过operation的生命周期通知外部客户的进展情况。自定义的子类需要维护该状态信息以确保代码中的操作正确的执行。 相关的operation状态的key path:
-
@property (readonly, getter=isReady) BOOL ready;
isReady
路径指示操作何时可以执行。ready 属性在操作准备好执行的时候为true,如果仍存在有未完成的操作依赖于它则为false。
-
@property(readonly, getter=isExecuting) BOOL executing;
isExecuting
属性指示操作是否正在处理其分配的任务。executing
属性在operation正在执行任务时为true,否则为false如果你自定义了operation对象的
start
方法,则当operation的执行状态更改时,还必须更新executing
属性并生成KVO通知。
-
@property(readonly, getter=isFinished) BOOL finished;
isFinished
路径指示某个操作已成功完成它的任务,或者已取消并正在退出。 operation对象不会清除一个依赖,直到isFinished
为true。同样的,一个操作队列不会把一个operation出列,直到finished
属性为true。因此,将operation标记为finished 对于防止队列进行备份或取消操作至关重要。如果你替换了
start
方法或操作对象,当操作结束执行或取消操作时生成KVO通知如果替换
start
方法或操作对象,则还必须替换finished
属性,并在操作结束执行或取消操作时生成KVO通知。
-
@property (readonly, getter=isCancelled) BOOL cancelled;
isCancelled
路径指示operation的取消被请求。不必为该路径发送KVO通知。
响应取消命令
一旦添加operation到队列中,operation就脱离你的控制了。队列会接管并处理任务的调度。但是,如果稍后决定不想执行operation,可能是因为用户点击取消按钮或退出了应用程序,比如,你可以取消operation来防止它不必要的占用CPU时间。可以通过operation对象的cancel
方法或- (void)cancelAllOperations;
方法。
取消一个operation不会立即强制停止正在执行的操作。代码中必须显式的检查该属性的值并根据需要中止。NSOperation
的默认实现包括检查取消状态。比如,如果在调用start
方法之前取消了一个操作,start
方法将退出而不启动该任务。
在任何自定义代码中,都应该始终支持取消语义。特别的,主要任务的代码应该定期性的检查cancelled
属性的值。如果为YES,operation对象应该尽可能快的清理并退出。如果实现了自定义的start
方法,那么该方法应该适当的提前检查取消状态。
除了当操作取消时退出操作,将取消的操作设置为一个合适的最终状态也是很重要的。如果我们自己管理finished
和executing
属性的值(可能是因为正在执行并发操作), 必须相应地更新这些属性。特别的,必须将finished
属性的值返回为YES,executing
属性的值为NO。即使操作在开始执行之前就被取消也必须进行这些更改。
几个方法的简单介绍
执行操作的方法
-
- (void)start;
开始执行操作。 该方法的默认实现会更改operation的执行状态,并调用接受者的main
方法。 该方法还会执行若干项检查确保操作能够实际运行。比如,如果receiver被取消或者已经finished,该方法将不会调用main
方法而直接return。如果操作正在执行或者尚未准备执行,该方法或抛出 NSInvalidArgumentException 异常。注意:
如果一个操作依赖的其他操作没有finished,则该认为该操作还没有准备好执行如果实现了并发操作,必须覆盖
start
方法,并使用该方法开启操作。该方法自定义的实现在任何时候都不应该调用 super。 除了为你的任务配置执行环境,该方法的自定义实现也必须跟踪operation 的状态并提供适当的状态转变。当操作执行并完成它的工作之后,还应该分别为isExecuting
和isFinished
路径生成KVO通知。如果想要手动执行操作的话可以显式的调用该方法。但是,对一个已经在操作队列里operation对象调用该方法,或者在调用该方法之后对操作进行排队,都是编程错误。一旦将operation添加到队列,队列将负责管理操作
-
- (void)main;
执行receiver的非并发任务。该方法的默认实现是实际上什么都没做。我们应该覆盖该方法来执行所需的任务。在自定义实现中,不应该调用super。该方法将会在NSOperation
提供的 autorelease pool 中自动执行,所以不需要我们创建 autorelease pool。如果要实现并发操作,不需要覆盖该方法。但是如果从自定义的
start
方法调用的话可能就需要重写该方法了 -
@property(copy) void (^completionBlock)(void);
操作主要任务完成之后执行的block。每当一个NSOperation执行完毕,它就会调用它的completionBlock属性一次。 该block的执行上下文并不确定,但通常是在子线程中。因此,不应该使用该block执行任何需要在特定执行上下文的工作。相反,应该该工作分配给主线程或者能够处理该任务的特定线程中。
当
finished
属性值为YES时,该block将执行。因为block在operation完成其任务后执行,所以不能使用该block来排队等待被认为是该任务一部分的其他任务。finished
属性为YES的operation对象必须完成它所有的任务。该block应该通知相应的对象工作已经完成,或者执行其他的一些任务(与operation实际任务相关,但不属于操作的一部分)在iOS 8以后和 macOS 10.10以后,在该block执行之后将该属性置为nil.
取消操作的方法
-
- (void)cancel;
通知operation对象停止执行其任务。该方法不会强制停止operation的代码。相反的,它更新对象的内部标志来反应状态的变化。如果操作已经finished,该方法将不会起作用。取消一个在操作队列中,但还没有开始执行的operation,可以更快地从队列中移除操作。
属性
-
@property(readonly, getter=isCancelled) BOOL cancelled;
指示操作是否已经取消的布尔值。
默认是NO,调用
cancel
方法将该属性设为YES。 一旦取消,operation必须转至finished状态。取消一个operation不会直接停止receiver代码的执行。如果返回YES,operation对象负责定期地调用该方法并自行停止。
在执行operation的任务之前,应该检查该属性的值。通常是在自定义的
main
方法中的开头处检查。在开始执行操作或者在执行的任意时候,操作都可能被取消。因此,在main
方法的开头检查该属性的值(并在该方法中定期的检查),可以在操作取消时尽可能快的退出。 -
@property(readonly, getter=isExecuting) BOOL executing;
指示操作是否正在执行的布尔值。
如果operation正在执行它的主任务,属性值为YES,否则为NO。
当实现了一个并发操作对象,必须覆盖该属性的实现,以便于可以返回操作对象的执行状态。在自定义的实现中,当操作对象的执行状态改变的时候必须为
isExecuting
路径生成KVO通知对于非并发操作不需要重新实现该属性
-
@property(readonly, getter=isFinished) BOOL finished;
指示操作是否已完成其任务。
如果operation已经完成它的主任务,该属性值为YES. 如果正在执行或还没有开始执行则为NO
当实现一个并发操作时,必须重写该属性的实现,以便返回operation的完成状态。在自定义实现中,如果操作的finished状态改变的话,必须为
isFinished
路径生成KVO通知。对于非并发操作无需重写该属性的实现
-
`@property(readonly, getter=isAsynchronous) BOOL asynchronous;
指示操作是否异步执行其任务。
对于当前线程异步执行的操作,该属性值为YES。对于当前线程同步执行的操作为NO。默认为NO
当实现一个异步操作对象,必须实现该属性并返回YES
-
@property(readonly, getter=isReady) BOOL ready;
指示现在是否可以执行操作的布尔值
操作的准备情况取决于它们对其他操作的依赖,以及你定义的自定义的条件。
NSOperation
类管理对其他操作的依赖关系,并根据这些依赖关系报告receiver的准备情况。如果想使用自定义条件来定义操作对象的准备情况,需要重写该属性的实现,并返回一个准确反映receiver的准备情况的值。如果这样做,自定义的实现必须从super获取默认的属性值,并将该值和冰岛该属性的新值中。在自定义实现中,当操作对象的准备状态改变的时候,必须为
isReady
路径生成KVO通知 -
@property(copy) NSString *name;
operation的名称。 为操作对象分配一个名称,方便debug。
管理依赖
-
- (void)addDependency:(NSOperation *)op;
使receiver依赖于指定操作的完成
参数:
operation: receiver应该依赖的操作。不应该向receiver重复添加相同的依赖,这样做的结果是undefined直到依赖的所有操作都执行结束后,receiver才会被认为准备好执行。如果receiver已经在执行其任务,添加依赖将不会有效果。该方法可能会改变receiver的
isReady
和dependencies
属性。注意不要造成循环依赖。这样做的话会造成操作间的死锁,并可能会freeze你的程序。
-
- (void)removeDependency:(NSOperation *)op;
移除receiver对特定操作的依赖。
该方法可能会更改receiver的
isReady
和dependencies
属性。 -
@property(readonly, copy) NSArray<NSOperation *> *dependencies;
在当前对象开始执行之前必须完成执行的操作对象数组。
该属性是包含的
NSOperation
对象的数组。使用addDependency:
方法为该数组添加对象。直到依赖的操作对象都执行结束后,operation才可以执行。操作完成执行时也不会从该列表中移除。可以使用该列表跟踪所有的依赖操作,包括那些已经完成的操作。唯一能从该列表中移除operation的方法是调用
removeDependency:
方法。
NSBlockOperation
NSBlockOperation类是NSOperation的一个具体子类,用于管理一个或多个block的并发执行。可以使用该对象一次执行数个block,而不必为每个block创建单独的操作对象。当执行多个block时,只有当所有的block执行结束之后,该操作对象才会被视为finished。
添加到NSBlockOperation的block以默认优先级调度到适当的工作队列。block本身不应该对其执行环境做任何假设
方法介绍
-
+ (instancetype)blockOperationWithBlock:(void (^)(void))block;
创建并返回一个NSBlockOperation对象,并为其添加指定的block。
-
- (void)addExecutionBlock:(void (^)(void))block;
添加指定block到receiver要执行的的block列表中。
指定的block不应该对它的执行环境做任何假设。
当receiver正在执行或者已经finished时调用该方法会抛出
NSInvalidArgumentException
异常 -
@property(readonly, copy) NSArray<void (^)(void)> *executionBlocks;
与receiver相关联的block数组
该数组中的block是调用
addExecutionBlock:
方法添加的block的副本。
NSInvocationOperation
NSInvocationOperation类是NSOperation的一个具体子类,用于开启一个操作,该操作包括在指定对象上调用一个selector。
方法
-
- (instancetype)initWithTarget:(id)target selector:(SEL)sel object:(id)arg;
参数:
target: 定义指定selector的对象。
sel: 运行该操作对象时会调用该selector。selector可能会有0或1个参数。如果有参数,参数的类型必须是 id。 方法的返回类型可以是 void、基本数据类型,或是可以作为id类型返回的对象。
arg: 传递给selector的参数对象。如果selector不需要参数,将arg置为nil即可。
该方法返回一个 NSInvocationOperation对象,或者nil(如果target对象没有实现指定的selector)
如果selector的返回类型是 非void,我们可以在操作完成执行后调用
result
方法来获取返回值。 -
- (instancetype)initWithInvocation:(NSInvocation *)inv;
使用指定的NSInvocation对象初始化一个NSInvocationOperation对象。
返回一个NSInvocationOperation对象或者nil(如果对象不能被初始化)
-
@property(readonly, retain) id result;
invocation 或方法的结果。
该方法返回的对象或者返回值的NSValue对象(如果返回值不是对象会转成NSValue对象)。如果方法或者invocation未完成执行,则为nil。
如果在执行方法或调用期间发生异常,访问该属性将会再次引发该异常。如果操作被取消或者invocation或方法返回类型为void,访问该属性也会引发异常。
NSOperation与GCD
尽管GCD对于内嵌异步操作十分理想,NSOperation依旧提供更复杂、面向对象的计算模型,它对于涉及到各种类型数据、需要重复处理的任务是更加理想的
什么时候使用GCD
Dispatch queues, groups, semaphores(信号量), sources, 和 barriers 组成了一组基本的并发基元,其上构建了所有的系统框架。
对于一次性的计算或简单的加速现有方法,使用轻量级的GCD比使用NSOperation更方便。
什么时候使用NSOperation
NSOperation 可以按照特定的队列优先级和服务质量(qualityOfService
)来调度一组依赖关系。与在GCD队列中的block不同, NSOperation可以被取消,以及查询其操作状态。通过继承,NSOperation可以将其工作结果与自身关联起来,供将来参考。
资料来源:
- 另外推荐一篇 Mattt大神的一篇文章:NSOperation,中文翻译请戳这里