NSOperation

简单介绍一下NSOperation

思考

operation,中文意译为行动/作业,同意词有task/action/work,思考一下如何用一个对象来描述。

一般的想法应该是这个对象调用特定的方法来完成某些任务,有一些属性来表示这个任务的状态,类似于下载一个文件一下,不过把行为固定为了下载,属性有开始下载、下载中、下载完成,或者有表示进度的属性等。

文档翻译

NSOperation

一个用来表示单个任务关联的代码和数据的抽象类。

概览

NSOperation是一个抽象类,不能直接使用,需要自定义它的子类或使用系统已定义的子类(NSInvocaionOperation、NSBlockOperation)来执行实际任务。尽管抽象,但是NSOperation的基本实现包含了协调任务可安全执行的重要逻辑。这种内置逻辑的存在使用户可以专注于任务的实际实现,而不是专注于其与其他系统对象正常工作所需的胶水代码。

一个operation对象是一个单发对象,它值执行一次任务。通常通过将操作添加到操作队列中(NSOperationQueue的实例)来执行操作。操作队列可以通过在辅助线程上运行它们来直接执行其操作,或使用libdispatch库(GCD)间接执行操作。

如不想使用操作队列,可以通过直接运行start方法来执行调用操作。手动执行操作需要增加额外的代码量,且启动未就绪状态的操作会触发异常。ready属性表示操作处于就绪状态。

操作依赖

依赖关系是按特定顺序执行操作的便利方式。可以使用addDependency: 和 removeDependency: 方法来添加和删除操作的依赖关系。默认情况下,一个具有依赖关系的操作对象直到其依赖的所有操作对象都执行完毕才会被视为就绪状态。一旦依赖的最后一个操作结束,操作对象立马就绪并可以执行。

NSOperation支持的依赖关系不关心其依赖操作是否成功或失败(即,取消操作与标记完成一样效果)。由使用者决定在取消依赖项操作或未成功完成任务的情况下是否应执行由依赖项的操作。这可能需要将一下错误跟踪功能合并到操作对象中。

符合KVO的属性

NSOperation类的某些属性可供KVC和KVO。根据需要,可以观察这些属性以控制应用程序的其他部分。

观察这些属性,使用下面的键路径:

  • isCancelled 只读
  • isAsynchronous 只读
  • isExecuting 只读
  • isFinished 只读
  • isReady 只读
  • dependencies 只读
  • queuePriority 读写
  • completionBlock 读写

尽管可以将观察者关联到这些属性,但不应该使用Cocoa bindings 去绑定它们到应用的用户界面的元素上。因为operation可能执行在任何线程,故与operation关联的KVO的通知可能在任何线程中发生。

如为上述任何属性提供自定义实现,则其实现必须保证符合KVO和KVC。如果为NSOperation对象定义其他属性,建议也使这些属性符合KVO和KVC。

多核的注意事项

NSOperation类本身是支持多核的。故可以安全地从多个线程调用NSOperation对象的方法,而无需创建额外的锁来同步访问其对象。因为一个操作通常与创建并监视它的线程分属不同线程,故这种行为是必须的。

当子类化NSOperation时,必须确保任何重写的方法都可以安全的从多个线程调用。如子类中实现了自定义方法(如自定义数据访问器),必须确保这些方法是线程安全的。因此必须同步访问操作中的任何数据,以防止潜在的数据损坏。

异步和同步操作

如果计划手动执行一个操作,并不是将其添加到队列中,则可将操作设计为以同步或异步方式执行。默认情况下,operation对象是同步的。在同步操作中,操作对象不会单独创建线程用于执行它的任务。当直接从代码中调用同步操作的start方法时,该操作在当前线程直接执行。当任务完成时,对象当start方法才将控制权返回给调用者。

当调用异步操作的start方法时,该方法可能在相应的任务完成之前返回。异步operation对象负责在单独的线程上调度其任务。operation可以通过调用异步方法或提交一个block到调度队列里执行来直接启动新线程。当控制权返回给调用者时,操作是否正在执行并不是很重要,只是操作可能正在进行中。

如果一直计划使用队列执行操作的话,将它们定义为同步方式会更简单点。如果手动执行操作的话,则可能需要将操作对象定义为异步类型。定义异步操作需要更多的工作,因为需要监视任务的进行中状态并使用KVO通知上报该状态的改变。在确保手动执行的操作不阻塞调用线程的情况下,定义异步操作是很有用的。

当将操作添加到操作队列中时,该队列将忽略异步属性的值并始终从单独线程中调用start方法。因此,如果始终通过将操作添加到操作队列中来运行操作的话,则没有理由使这些操作是异步方式的。

子类化的注意事项

NSOperation类提供了跟踪操作的执行状态的基本逻辑,但必须将其子类化才能进行实际的工作。如何创建子类取决于设计的操作是并行执行还是同步执行。

方法重写

对于非并行操作,通常只要重写一个方法: main

在此方法中,写上执行给定任务所需的代码即可。当然,还需要自定义一个初始化方法以便更简单的创建自定义的操作类的实例。可能还需要定义getter和setter方法来访问operation中的数据。但是如果自定义来getter/setter方法,则必须确保这些方法可以在多个线程中被安全调用。

如果要创建并发操作,则至少需要重写一下方法和属性:start、asynchronous、executing、finished

在并发操作中,start方法负责以异步方式启动该操作。通过此方法可以生成线程或调用异步函数。启动操作后,start方法还应该以上报executing属性的方式更新该操作的执行状态。可以通过发送executing的键路径的KVO通知的方式,使对此感兴趣的客户知道操作现在正在执行中。executing属性也必须以线程安全的方式提供状态。

当完成或取消任务时,并发操作对象需要为isExecuting/isFinished键路径生成KVO通知,并标记操作状态当最终变更(在取消的情况下,即使操作的任务未完全结束,但更新isFinished键路径仍然很重要。队列的操作必须上报它们已经结束,然后才能从队列里移除)。除了生成KVO通知外,需要重写executing/finished属性也应该继续根据操作的状态上报准备的值。

重要 start方法中,任何时候都不应该调用super。当定义并发操作时,需要自己提供与默认start方法相同的行为,包括启动任务和生成适当的KVO通知。start方法还应该在实际启动任务之前检查操作本身是否已取消。

即使对于并发操作,除上述方法外,也几乎不需要重写其他方法。但是,如果自定义操作的依赖特性,则可能必须重写额外的方法并提供KVO通知。对于依赖项,可能仅需要为isReady键路径提供通知。因为dependencies属性包含依赖项操作的列表,所以对它的更改已经由默认的NSOperation类处理了。

维护操作对象状态

操作对象在内部维护状态信息以便确定何时可以安全的执行,并在操作的整个生命周期内将进度情况通知外部客户。自定义子类维护这些状态信息以确保代码中正确的执行操作。与一个操作状态关联的键路径有:

isReady

isReady键路径可以让客户知道何时一个操作已准备好执行。当操作准备好立即执行时,ready属性包含值为true;如果还有未完成的依赖操作项时,值为false。

大多数情况下,不必自己管理此键路径的状态。但是,如果操作的就绪状态是由其依赖操作项以外的因素决定的,比如有程序中的某些外部条件决定的,则可以自己提供ready属性的实现,并自己跟踪操作的就绪状态。尽管一般情况下仅在外部状态允许时再创建操作对象更简单一点。

macOS 10.6以上版本,如果在等待一个或多个依赖项操作时取消当前操作,则这些依赖关系将被忽略,并且此属性的值将更新以反映它现在可以运行了。此行为使操作队列有机会更快地将已取消的操作从队列中移除。

isExecuting

isExecuting键路径可以让客户知道操作是否正在执行其分配的任务。如果操作正在执行其任务,executing属性必须上报值为true,反之为false。

如果替换操作对象的start方法,在操作的执行状态变更时必须替换executing属性,并生成KVO通知。

isFinished

isFinished键路径可以让客户知道一个操作的任务已经完成或已被取消并正在退出。在isFinished键路径的值改为true之前,操作对象不会清除其依赖关系。同样的,在finished属性值为true之前,操作队列不会使此操作出队。因此,将操作标记为已完成对于防止队列进行中或已取消的操作进行备份是很重要的。

如替换start方法或操作对象,则必须也替换finished属性,并在操作结束执行或取消时生成KVO通知。

isCancelled

isCancelled键路径使客户知道取消操作被请求。取消支持是自愿的,但鼓励这么做,您自己的代码不必为此键路径发送KVO通知。

响应cancel指令

当操作添加添加到一个队列中时,这个操作将不受你的处理。队列接管并处理该任务的调度。但是,如果以后决定不想执行该操作(比如用户按下了进度面板中的“取消”按钮或退出了该应用程序),则可以取消这个操作以防止其不需要地消耗CPU时间。可以通过调用操作对象本身的cancel方法或通过NSOperationQueue类的cancelAllOperation方法来执行此操作。

取消操作并不会立即强制停止正在执行的操作。尽管关于所有操作中canceled属性中的值都预期存在,但是代码中应该显式地检查此属性值并根据需要终止操作。NSOperation的默认实现包括检查取消任务。比如,如在调用操作的start方法之前取消了操作,start方法将退出并不启动任务。

注意 在macOS 10.6即更高版本中,如果对操作队列中具有未完成对从属操作的操作调用cancel方法,则这些从属操作随后将被忽略。因为该操作已被取消,此行为允许队列调用该操作的start方法来从该队列中删除该操作,而无需调用其main方法。如果对不在队列中的操作调用cancel方法,则该操作将立即标记为已取消。在每种情况下,将操作标记为就绪或已完成都会生成适当的KVO通知。

在编写任何自定义代码中应该始终支持取消语义。特别是主要任务代码应该定期检查cancelled属性值。如属性值为YES,操作对象应尽快进行清理和退出。如果实现了自定义start方法,则该方法应该包括取消行为的早期检查并表现优雅一点。自定义start方法必须准备处理这种提前取消的情况。

除了在取消操作时简单的退出之外,将取消的操作迁移至最终的状态也很重要。比如自己关系finished和executing属性的值(可能因为你正在执行并发操作),则需要相应的更新这些属性。比如必须将finished的返回值改为YES,将executing的返回值改为NO。即使在操作开始执行之前取消了该操作,也必须进行这些变更。

NSOperationQueue

是一个用来规范操作执行的队列。

概览

操作队列是根据排队中的NSOperation对象的优先级和准备状态来执行它们的。在将一个操作添加到操作队列中后,该操作将保留在队列中,直到它报告其完成了任务为止。将操作添加到队列中后,将无法直接从队列中删除该操作。

注意 操作队列持有操作直到操作任务完成,队列本身将被保留直到所有的操作结束。用未完成的操作来暂停操作队列可能导致内存泄漏。

确定执行顺序

队列中的操作根据其就绪情况,优先级和操作间的依赖关系进行组织,并相应的执行。如果所有排队中的操作都具有相同的队列优先级并当它们放入队列时已准备好执行(即ready属性返回YES),则将按照它们提交到队列的顺序执行它们。否则,操作队列始终执行相对于其他就绪操作而言优先级最高的操作。

但是,永远不应该依赖队列语义来确保操作的特定顺序,因为操作的就绪状态的改变会修改执行的顺序。操作间的依赖性为操作提供了绝对的执行顺序,即使这些操作位于不同的操作队列中。一个操作对象的所有依赖操作都结束执行后才认为该操作对象处于就绪执行状态。

取消操作

结束当前任务并不一定意味着该操作执行任务完成;一个操作也可以被取消。取消操作对象会将该对象保留在队列中,但是会通知该对象它应该尽快停止其任务。对于正在执行的操作,这意味着操作对象的工作代码需要检查取消状态,停止正在执行的操作,并标记自己为已结束状态。对于已排队但还没有执行的操作,队列仍必须调用操作对象的start方法,以便它可以处理取消事件并标记自身为已结束状态。

注意 取消操作会导致该操作忽略其可能具有的依赖关系。这种行为使得队列可以尽快的执行操作的start方法。依次使用start方法将操作迁移到完成状态,以便可以将其从队列中移除。

符合KVO的属性

NSOperationQueue类符合KVC(键值编码)和KVO(键值观察)。可以根据需要观察这些属性,以便控制应用程序的其他部分。

可供观察属性的键路径有:

  • operations - 只读
  • operationCount - 只读
  • maxConcurrentOpeationCount - 可读写
  • suspended - 可读写
  • name - 可读写

尽管可以将观察者附加到这些属性,但不应该使用Cocoa绑定将它们绑定到应用程序的用户界面元素上。与线程相关的代码通常只能在应用程序的主线程中执行。但是,KVO通知关联的操作队列可能在任何线程发出。

线程安全

在多个线程中使用单个NSOperationQueue对象是线程安全的,无需创建额外的锁来同步访问该对象。操作队列使用Dispatch框架来启动其操作的执行。所以,操作始终在单独的线程上执行,不论操作被指定为同步或异步。

结论

引用源

Written on September 29, 2019