NSOperation的进阶使用和简单探讨


本文将会从这多个方面探讨NSOperation类和NSOperationQueue类的相关内容

一、简介

NSOperation的是iOS2.0推出的,通过NSThread实现的,但是效率的确一般。
从OS X10.6和iOS4推出GCD时,又重写了NSOperation和NSOperationQueue,NSOperation和NSOperationQueue分别对应GCD的任务和队列,所以NSOPeration和NSOperationQueue是基于GCD更高一层的封装,而且完全地面向对象。但是比GCD更简单易用、代码可读性也更高。NSOperation和NSOperationQueue对比GCD会带来一点额外的系统开销,但是可以在多个操作Operation中添加附属。

二、知识概括

从NSOperation的思维导图了解的这个类相关的整体的知识点:

NSOperation和NSOperationQueue是基于GCD的更高一层的封装,分别对应GCD的任务和队列,完全地面向对象。可以通过start方法直接启动NSOperation子类对象,并且默认同步执行任务,将NSOperation子类对象添加到NSOperationQueue中,该队列默认并发的调度任务。

开启操作有二种方式,一是通过start方法直接启动操作,该操作默认同步执行,二是将操作添加到NSOperationQueue中,然后由系统从队列中获取操作然后添加到一个新线程中执行,这些操作默认并发执行。

具体实现如下:
方式一:直接由NSOperation子类对象启动。
首先将需要执行的操作封装到NSOperation子类对象中,然后该对象调用Start方法。
方式二:当添加到NSOperationQueue对象中,由该队列对象启动操作。

  1. 将需要执行的操作封装到NSOperation子类对象中
  2. 将该对象添加到NSOperationQueue中
  3. 系统将NSOperation子类对象从NSOperationQueue中取出
  4. 将取出的操作放到一个新线程中执行

使用队列来执行操作,分为2个阶段:第一阶段:添加到线程队列的过程,是上图的步骤1和2。第二阶段:系统自动从队列中取出线程,并且自动放到线程中执行,是上图的步骤3和4。

接下来相关内容的总结:

1. NSOperation

NSOperation是一个和任务相关的抽象类,不具备封装操作的能力,必须使用其子类。
使用NSOperation⼦类的方式有3种:

  • 系统实现的具体子类:NSInvocationOperation
  • 系统实现的具体子类:NSBlockOperation
  • 自定义子类,实现内部相应的⽅法
    该类是线程安全的,不必管理线程生命周期和同步等问题。

a. NSInvocationOperation子类
  NSInvocationOperation是NSOperation的子类。创建操作对象的方式有2种,使用initWithTarget:selector:object:创建sel参数是一个或0个的操作对象。使用initWithInvocation:方法,添加sel参数是0个或多个操作对象。在未添加到队列的情况下,创建操作对象的过程中不会开辟线程,会在当前线程中执行同步操作。创建完成后,直接调用start方法,会启动操作对象来执行操,或者添加到NSOperationQueue队列中。无论使用该子类的哪个在初始化的方法,都会在添加一个任务。 和NSBlockOperation子类不同的是,因为没有额外添加任务的方法,使用NSInvocationOperation创建的对象只会有一个任务。
  默认情况下,调用start方法不会开辟一个新线程去执行操作,而是在当前线程同步执行任务。只有将其放到一个NSOperationQueue中,才会异步执行操作

b. NSBlockOperation子类
可以通过blockOperationWithBlock:创建NSBlockOperation对象,在创建的时候也添加一个任务。如果想添加更多的任务,可以使用addExecutionBlock:方法。也可以通过init:创建NSBlockOperation对象。但是这种创建方式并不会在创建对象的时候添加任务,同样可以使用addExecutionBlock:方法添加任务。对于启动操作和NSInvocationOperation类一样,都可以通过调用start方法和添加NSOperationQueue中来执行操作。

关于任务的的同步、异步的执行可以总结几点:

  1. 任务数为1时,即使用blockOperationWithBlock:方法或者init:addExecutionBlock:二个方法结合的方式创建的唯一一个任务时,不会开辟新线程,直接在当前线程同步执行任务。
  2. 任务数大于1时,使用blockOperationWithBlock:方法或者init:addExecutionBlock:二个方法结合的方式创建的一个任务A,不会开辟线程,直接在当前线程同步执行任务。而NSBlockOperation对象使用addExecutionBlock:方法添加的其他任务会开辟新线程,异步执行任务。
  3. 将操作放到一个NSOperationQueue中,会异步执行操作任务。

注意:不可在completionBlock属性的block中追加任务,因为在操作已经启动执行中或者结束后不可以添加block任务。

c. 自定义子类
  一般类NSInvocationOperation、NSBlockOperation就可以满足使用需要,当然还可以自己自定义子类。
  创建的子类时,需要考虑到可能会添加到串行和并发队列的不同情况,需要重写不同的方法。对于串行操作,仅仅需要重新main方法就行,在这个方法中添加想要实现的功能。对于并发操作,重写四个方法:startasynchronousexecutingfinished。并且需要自己创建自动释放池,因为异步操作无法访问主线程的自动释放池。

注意:在自定义子类时,经常通过cancelled属性检查方法是否取消,并且对取消的做出响应。

2. NSOperationQueue

使用将NSOperation对象添加NSOperationQueue中,来管理操作对象是非常方便的。因为当我们把操作对象添加到NSOperationQueue对象后,该NSOperationQueue对象从线程中拿取操作、以及分配到对应线程的工作都是由系统处理的。
只要是创建了队列,在队列中的操作,就会在子线程中执行,并且默认并发操作。添加到子队列NSOperationQueue实例中的操作,都是异步执行

a.操作对象添加到NSOperationQueue对象中
添加的方式有3种。

  • addOperation:添加一个操作
  • addOperationWithBlock:,系统自动封装成一个NSBlockOperation对象,然后添加到队列中
  • addOperations:waitUntilFinished:添加多个操作
    操作对象添加到NSOperationQueue之后,通常短时间内就会运行。但是如果存在依赖,或者整个队列被暂停等原因,也可能需要等待。

操作对象添加NSOperationQueue中后,不要再修改操作对象的状态。因为操作对象可能会在任何时候运行,因此改变操作对象的依赖或数据会产生无法预估的问题。只能查看操作对象的状态, 比如是否正在运行、等待运行、已经完成等。

b. 设置最多并发数
  不要开辟太多线程,因为开辟线程需要消耗CPU资源。一般以2~3为宜,虽然任务可以在子线程处理,但是cpu处理过多的子线程可能会让UI变卡顿。
  通过maxConcurrentOperationCount属性,设置了最多并发数,就可以主动的控制开辟线程的个数了,这是NSThread和GCD都不具体的功能。当最多并发数为一,就是个串行队列了。当然这个GCD的串行队列还是有些不同的。因为约束执行顺序的还有依赖和优先级等因素。maxConcurrentOperationCount默认是-1,不可设置为0。如果没有设置最大并发数,那么并发的个数是由系统内存和CPU决定的。
  虽然NSOperationQueue类设计用于并发执行操作,但是也可以强制让单个队列一次只能执行一个操作。setMaxConcurrentOperationCount:方法可以设置队列的最大并发操作数量。当设为1就表示NSOperationQueue实例每次只能执行一个NSOperation子类对象。不过操作对象执行的顺序会依赖于其它因素,比如操作是否准备好和操作对象的优先级等。因此串行化的operation queue并不等同于GCD中的串行dispatch queue。

相关概念:

  1. 并发数:同时执⾏的任务数,即队列中同时运行的线程数。eg,同时开4个线程执行4个任务,并发数就是4。
  2. 最大并发数:同一时间最多能执行的任务的个数。即队列中最多同时运行的线程数。

c. 进度修改
  一个操作执行还未完成时,我们可能需要让该任务暂停、可能之后在进行某些操作后又希望继续执行。为了满足这个需要,苹果公司,为我们提供了suspended属性。当可能我们不想执行某些操作时,可以个cancel方法、cancelAllOperations方法可以取消操作对象,一旦调用了这2个方法,操作对象将无法恢复。具体如下:
  对于暂停操作,当NSOperationQueue对象属性suspended设置为YES,队列会停止对任务调度。对那些还在线程中的操作有影响的。如果任务正在执行将不会受到影响,因为任务已经被队列调度到一个线程上并执行。
  对于继续操作,当属性suspended设置为NO会继续执行线程操作。队列将积极启动队列中已准备执行的操作。
  一旦NSOperation子类操作对象添加到NSOperationQueue对象中,该队列就拥有了该操作对象并且不能删除操作对象,如果不想执行操作对象,只能取消该操作对象。关于取消操作,可以分为2种情况,取消一个操作和取消一个队列的全部操作二种情况。调用NSOperation类实例的cancel方法取消单个操作对象。调用NSOperationQueue类实例cancelAllOperations方法取消队列中全部操作对象。
  对于队列中的操作,只有操作标记为已结束才能被队列移除。在队列中未被调度的操作,会调用start方法执行操作,以便操作对象处理取消事件。然后标记这些操作对象为已结束。对于正在线程中执行其任务的操作对象,正在执行的任务会继续执行,该操作对象会被标记经结束。
注意:只会停止调度队列中操作对象,正在执行任务的依然会执行,且取消不可恢复。

d.作用
  NSOperation对象可以调⽤start⽅法来执⾏任务,但默认是同步执行的(可以创建异步操作,NSBlockOperation添加操作数大于1时,除第一个任务外,其任务就是异步执行)。如果将NSOperation添加到NSOperationQueue中,之后操作就就由系统管理,系统先从队列中取出操作,然后放到一个新线程中异步执行。总结:添加操作到NSOperationQueue中,自动执行操作,自动开启线程

f. 获取队列
  系统提供了2个,可以获取当前队列和主队列。可以通过类属性currentQueue获取当前队列。可以通过类属性mainQueue获取主队列.

3.依赖

操作对象可以添加和移除依赖。当一个操作对象添加了依赖,被依赖的操作对象就会先执行,当被依赖的操作对象执行完才会当前的操作对象。添加到不同线程对象中的操作对象依然彼此间可以单方面依赖。切记循环依赖的情况。这样会产生死循环。
  可以通过addDependency方法添加一个或者多个依赖的对象。eg:[A addDependency:B]; 操作A依赖于操作B。操作对象会管理自己的依赖,因此在不相同队列中的操作对象可以建立依赖关系。但是**一定要在添加线程对象NSOperationQueue之前,进行依赖设置。**设置依赖可以保证执行顺序,操作添加到队列添加的顺序并不能决定执行顺序,执行的顺序取决于多种因素比如依赖、优先级等。
  调用removeDependency:方法移除依赖。

如图,箭头方向就是依赖的对象,从图中可知,A依赖b,而b依赖C。所以三者的执行顺序是C–>b–>A

4.线程安全

在NSOperation实例在多线程上执行是安全的,不需要添加额外的锁

5.cancel方法

只会对未执行的操作有效,正在执行的操作,在收到cancel消息后,依然会执行。
  调用操作队列中的操作的cancel方法,且该操作队列具有未完成的依赖操作时,那么这些依赖操作会被忽略。由于操作已经被取消,因此此行为允许队列调用操作的start方法,以便在不调用其主方法的情况下从队列中删除操作。如果对不在队列中的操作调用cancel方法,则该操作立即标记为已取消。

6.状态属性

一个线程有未创建、就绪、运行中、阻塞、消亡等多个状态。而操作对象也有多种状态:executing(执行中)、finished(完成)、ready(就绪)状态,这三个属性是苹果公司,提供给我们用于观察操作对象状态的时候用的。因为这个三个属性KVC与KVO兼容的,因此可以监听操作对象状态属性。

7.操作完成

a. 监听操作完成
当我们可能需要在某个操作对象完成后添加一些功能,此时就可以用属性completionBlock来添加额外的内容了。

operation.completionBlock = ^{
  // 完成操作后,可以追加的内容
};

b. 等待操作完成
这个有2种情况:一是等待单个操作对象,而是等待队列里全部的操作。
  如果想等待整个队列的操作,可以同时等待一个queue中的所有操作。使用NSOperationQueue的waitUntilAllOperationsAreFinished方法。在等待一个队列时,应用的其它线程仍然可以往队列中添加操作,因此可能会加长线程的等待时间。

// 阻塞当前线程,等待queue的所有操作执行完毕
[queue waitUntilAllOperationsAreFinished];

对于单个操作对象,为了最佳的性能,尽可能设计异步操作,这样可以让应用在正在执行操作时可以去处理其它事情。如果需要当前线程操作对象处理完成后的结果,可以使用NSOperation的waitUntilFinished方法阻塞当前线程,等待操作完成。通常应该避免这样编写,阻塞当前线程可能是一种简便的解决方案,但是它引入了更多的串行代码,限制了整个应用的并发性,同时也降低了用户体验。绝对不要在应用主线程中等待一个Operation,只能在非中等待。因为阻塞主线程将导致应用无法响应用户事件,应用也将表现为无响应。

// 会阻塞当前线程,等到某个operation执行完毕
[operation waitUntilFinished];

8.执行顺序

添加到NSOperationQueue中的操作对象,其执行顺序取决于2点:
1.首先判断操作对象是否已经准备好:由对象的依赖关系确定
2.然后再根据所有操作对象的相对优先级来确定:优先级等级则是操作对象本身的一个属性。默认所有操作对象都拥有“普通”优先级,不过可以通过qualityOfService:方法来提升或降低操作对象的优先级。优先级只能应用于相同队列中的操作对象。如果应用有多个操作队列,每个队列的优先级等级是互相独立的。因此不同队列中的低优先级操作仍然可能比高优先级操作更早执行。

对于优先级,我们可以使用属性queuePriority给某个操作对象设置高底,优先级高的任务,调用的几率会更大, 并不能保证执行顺序。并且优先级不能替代依赖关系,优先级只是对已经准备好的操作对象确定执行顺序。先满足依赖关系,然后再根据优先级从所有准备好的操作中选择优先级最高的那个执行。

9.服务质量

根据CPU,网络和磁盘的分配来创建一个操作的系统优先级。一个高质量的服务就意味着更多的资源得以提供来更快的完成操作。涉及到CPU调度的优先级、IO优先级、任务运行所在的线程以及运行的顺序等等

通过设置属性qualityOfService来设置服务质量。QoS 有五种优先级,默认为NSQualityOfServiceDefault。它的出现统一了Cocoa中所有多线程技术的优先级。在此之前,NSOperation和NSThread都通过threadPriority来指定优先级,而 GCD 则是根据 DISPATCH_QUEUE_PRIORITY_DEFAULT 等宏定义的整形数来指定优先级。正确的使用新的 QoS 来指定线程或任务优先级可以让 iOS 更加智能的分配硬件资源,以便于提高执行效率和控制电量。

三、相关类介绍

NSOperation

NSOperation是NSObject的子类,表示单个工作单元。它是一个与任务的相关抽象类,为状态、优先级、依赖关系和管理提供了一个有用的、线程安全的结构。

创建自定义NSOperation子类是没有意义的,Foundation提供了具体的实现的子类:NSBlockOperation和NSInvocationOperation。

适合于NSOperation的任务的例子包括网络请求、图像调整、文本处理或任何其他可重复的、结构化的、长时间运行的任务,这些任务会产生关联的状态或数据。

概观

因为NSOperation类是一个抽象类,并不具备封装操作的能力,所以不能直接使用该类,而是应该使用其子类来执行实际的任务。其子类包括2种,系统定义的子类(NSInvocationOperationNSBlockOperation)和自定义的子类。虽然NSOperation类是抽象类,但是该类的基本实现中包括了安全执行任务的重要逻辑。这个内置逻辑的存在可以让你专注于任务的实际实现,而不是专注于编写能保证它与其他系统对象的正常工作的粘合代码。

一个操作对象是一个单发对象,也就是说,一旦它执行了其任务,将不能再执行一遍。通常通过添加他们到一个操作队列(NSOperationQueue类的一个实例)中来执行操作。操作队列通过让操作在辅助线程(非主线程)上运行,或间接使用libdispatch库(也称为GCD)直接来执行其操作。

如果不想使用一个操作队列,可以调用start方法直接来执行一个操作。手动执行操作会增加更多的代码负担,因为开启不在就绪状态的操作会引发异常。ready属性表示操作的就绪状态。

操作依赖

依赖是一种按照特定顺序执行操作的便捷方式。可以使用addDependency:removeDependency:方法给操作添加和删除依赖。默认情况下,直到具有依赖的操作对象的所有依赖都执行完成才会认为操作对象是ready(就绪)状态,。一旦最后一个依赖操作完成,这个操作对象会变成就绪状态并且可以执行。

NSOperation支持的依赖是不会区分其操作是成功的完成还是失败的完成。(换句话说,取消操作也视为完成。)由你来决定有依赖的操作在其所依赖的操作被取消或没有成功完成任务的情况下是否应该继续。这可能需要合并一些额外的错误跟踪功能到操作对象里。

兼容KVO的属性

NSOperation类对其一些属性是键值编码(KVC)和键值观察(KVO)兼容的。如有需要,可以观察这些属性来控制应用程序的其他部分。使用以下键路径来观察属性:

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

虽然可以为这些属性添加观察者,但是不应该使用Cocoa bindings来把它们和用户界面相关的元素绑定。用户界面相关的代码通常只有在应用程序的主线程中执行。因为一个操作可以在任何线程上执行,该操作KVO通知同样可能发生在任何线程。

如果你为之前的属性提供了自定义的实现,那么该实现内容必须保持与KVC和KVO的兼容。如果你为NSOperation对象定义了额外的属性,建议你同样需要让这些属性保持KVC和KVO兼容。

多核注意事项

可以从多线程中安全地调用NSOperation对象的方法而不需要创建额外的锁来同步存取对象。这种行为是必要的,因为一个操作的创建和监控通常在一个单独的线程上。

当子类化NSOperation类时,必须确保任何重写的方法能在多个线程中是安全的调用。如果实现子类中自定义方法,比如自定义数据访问器(accessors,getter),必须确保这些方法是线程安全的。因此,访问任何数据变量的操作必须同步,以防止潜在的数据损坏。更多关于信息同步的,可以查看Threading Programming Guide

异步操作 VS 同步操作

如果想要手动执行操作对象而不是将其添加到一个队列中,那么可以设计同步或异步的二种方式来执行操作。操作对象默认是同步的。在同步操作中,操作对象不会创建一个单独的线程来运行它的任务。当直接调用同步操作的start方法时,该操作会在当前线程中立即执行。等到这个对象的开始(start)方法返回给调用者时,表示该任务完成。

当你调用一个异步操作的start方法时,该方法可能在相应的任务完成前返回。一个异步操作对象负责在一个单独线程上调度任务。通过直接开启新一个线程、调用一个异步方法,或者提交一个block到调度队列来执行这个操作。一个异步操作对象可以直接启动一个新线程。(具有开辟新线程的能力,但是不一定就好开启新线程,因为CPU资源有限,不可能开启无限个线程)

如果使用队列来执行操作,将他们定义为同步操作是非常简单的。如果手动执行操作,可以将操作对象定义为异步的。定义一个异步操作需要更多的工作,因为你必须监控正在进行的任务的状态和使用报告KVO通知状态的变化。但在你想确保手动执行操作不会阻塞调用线程的情况下定义异步操作是特别有用的。

当添加一个操作到一个操作队列中,队列中操作会忽略了asynchronous属性的值,总是从一个单独的线程调用start方法。因此,如果你总是通过把操作添加到操作队列来运行操作,没有理由让他们异步的。

子类化注释

NSOperation类提供了基本的逻辑来跟踪操作的执行状态,但必须从它派生出子类做实际工作。如何创建子类依赖于该子类设计用于并发还是非并发。

方法的重载

对于非并发操作,通常只覆盖一个方法

  • main

在该方法中,需要给执行特定的任务添加必要的代码。当然,也可以定义一个自定义的初始化方法,让它更容易创建自定义类的实例。你可能还想定义getter和setter方法来从操作访问数据。然而,如果你定义定制了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

    该键路径让客户端知道一个操作何时可以准备执行。当操作马上可以执行时该属性值为true,当其依赖中有未完成,则是false。
    大多数情况下,没必要自己管理这个键路径的状态。如果操作的就绪状态是由操作依赖因素决定(例如在你的程序中的一些外部条件),那么你可以提供ready属性的实现并且跟踪操作的就绪状态。虽然只在外部状态允许的情况下创建操作对象时通常更简单。

    在macOS 10.6或更高版本中,如果取消的操作,正在等待一个或多个依赖操作完成,那么这些依赖项将被忽略,该属性的值将更新成已经准备好运行了。这种行为使操作队列有机会更快地将已取消的操作从队列中清除出去。

  • isExecuting

    该键路径让客户端知道操作是否在正在地执行它所分配的任务。如果操作正在处理其任务,则值为true;否则值为false。

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

  • isFinished

    该键路径让客户端知道操作成功地完成了任务或者被取消并退出。直到isFinished这个键路径的值变为true,操作对象才会清除依赖。类似的,直到finished属性的是true时,一个操作队列才会退出操作队列。因此,将操作标记为已完成对于防止队列备份正在进行的操作或已取消的操作非常重要。

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

  • isCancelled

    isCancelled键路径让客户端知道请求取消某个操作。支持自愿取消,但不鼓励主动发送这个键路径的KVO通知。

响应取消命令

一旦将操作添加到队列中,操作就不在你的控制范围内了。队列接管并处理该任务的调度。但是,如果你最终决定不想执行某些操作,例如用户按下取消按钮或退出应用程序时,你可以取消操作,以防止消耗不必要地CPU时间。可以通过调用操作对象本身的cancel方法或调用NSOperationQueue类的cancelAllOperations方法来实现这一点。

取消一个操作不会立即迫使它停止它正在做的事情。虽然所有操作都需要考虑cancelled属性中的值,但是必须显式检查该属性中的值,并根据需要中止。NSOperation的默认实现包括取消检查。例如,如果在调用一个操作的start方法之前取消该操作,那么start方法将退出而不启动任务。

提示

在macOS 10.6或更高版本中,如果调用操作队列中的操作的cancel方法,且该操作队列具有未完成的依赖操作,那么这些依赖操作随后将被忽略。由于操作已经被取消,因此此行为允许队列调用操作的start方法,以便在不调用其主方法的情况下从队列中删除操作。如果对不在队列中的操作调用cancel方法,则该操作立即标记为已取消。在每种情况下,将操作标记为已准备好或已完成时,会生成适当的KVO通知。

在你编写的任何定制代码中,都应该始终支持取消语义。特别是,主任务代码应该定期检查cancelled属性的值。如果属性值为YES,则操作对象应该尽快清理并退出。如果您实现了一个自定义的start方法,那么该方法应该包含早期的取消检查并适当地执行。您的自定义开始方法必须准备好处理这种类型的提前取消。

除了在操作被取消时简单地退出之外,将已取消的操作移动到适当的最终状态也很重要。具体来说,如果您自己管理finished和executing属性的值(可能是因为你正在实现并发操作),那么你必须更新更新相应地属性。具体来说,你必须将finished返回的值更改为YES,将executing返回的值更改为NO。即使操作在开始执行之前被取消,你也必须进行这些更改。

属性和方法

初始化

// 返回一个初始化的NSOperation对象
- (instancetype)init;// 父类 NSObject方法

执行操作

// 开启操作
//在当前任务状态和依赖关系合适的情况下,启动NSOperation的main方法任务,需要注意缺省实现只是在当前线程运行。如果需要并发执行,子类必须重写这个方法,并且使属性asynchronous返回YES。
- (void)start;
// 执行接收者(NSOperation)的非并发任务。操作任务的入口,一般用于自定义NSOperation的子类
- (void)main;
// 操作主任务完成后执行这个block
// 由于NSOperation有可能被取消,所以在block运行的代码应该和NSOperation的核心任务无关
@property (nullable, copy) void (^completionBlock)(void);

取消操作

// 通知操作对象(NSOperation)停止执行其任务。标记isCancelled状态。
// 调用后不会自动马上取消,需要通过isCancelled方法检查是否被取消,然后自己编写代码退出当前的操作
- (void)cancel;

获取操作状态

// Boolean 值,表示操作是否已经取消
@property (readonly, getter=isCancelled) BOOL cancelled;
// Boolean 值,表示操作是否正在执行
@property (readonly, getter=isExecuting) BOOL executing;
// Boolean 值,表示操作是否正完成执行
@property (readonly, getter=isFinished) BOOL finished;
// Boolean 值,表示操作是否异步执行任务
@property (readonly, getter=isAsynchronous) BOOL asynchronous ;
// Boolean 值,表示操作是否可以立即执行(准备完毕状态)
@property (readonly, getter=isReady) BOOL ready;
// 操作的名字
@property (nullable, copy) NSString *name;

管理依赖

// 添加依赖,使接收器依赖于指定完成操作。
// 如:[op1 addDependency:op2]; op2先执行,op1后执行
- (void)addDependency:(NSOperation *)op;

// 取消依赖,移出接收方对指定操作的依赖
// 注意:操作对象的依赖不能在操作队列执行时取消
- (void)removeDependency:(NSOperation *)op;

// 在当前对象开始执行之前必须完成执行的操作对象数组。
@property (readonly, copy) NSArray<NSOperation *> *dependencies;

执行优先级

// 操作获取系统资源的相对的重要性。系统自动合理的管理队列的资源分配
@property NSQualityOfService qualityOfService;

等待一个操作对象

// 阻塞当前线程的执行,直到操作对象完成其任务。可用于线程执行顺序的同步。
- (void)waitUntilFinished;

常量

// 这些常量允许您对执行操作的顺序进行优先排序。
NSOperationQueuePriority
// 用于表示工作对系统的性质和重要性。服务质量较高的类比服务质量较低的类获得更多的资源。
NSQualityOfService
// NSOperation优先级的枚举
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
	NSOperationQueuePriorityVeryLow = -8L,
	NSOperationQueuePriorityLow = -4L,
	NSOperationQueuePriorityNormal = 0,
	NSOperationQueuePriorityHigh = 4,
	NSOperationQueuePriorityVeryHigh = 8
};

在iOS8之后苹果提供了几个Quality of Service枚举来使用:user interactive, user initiated, utility 和 background。通过这些枚举告诉系统我们在进行什么样的工作,然后系统会通过合理的资源控制来最高效的执行任务代码,其中主要涉及到CPU调度的优先级、IO优先级、任务运行在哪个线程以及运行的顺序等等,我们可以通过一个抽象的Quality of Service枚举参数来表明任务的意图以及类别

//与用户交互的任务,这些任务通常跟UI级别的刷新相关,比如动画,这些任务需要在一瞬间完成.
NSQualityOfServiceUserInteractive
// 由用户发起的并且需要立即得到结果的任务,比如滑动scroll view时去加载数据用于后续cell的显示,这些任务通常跟后续的用户交互相关,在几秒或者更短的时间内完成
NSQualityOfServiceUserInitiated
// 一些可能需要花点时间的任务,这些任务不需要马上返回结果,比如下载的任务,这些任务可能花费几秒或者几分钟的时间
NSQualityOfServiceUtility
// 一些可能需要花点时间的任务,这些任务不需要马上返回结果,比如下载的任务,这些任务可能花费几秒或者几分钟的时间
NSQualityOfServiceBackground
// 一些可能需要花点时间的任务,这些任务不需要马上返回结果,比如下载的任务,这些任务可能花费几秒或者几分钟的时间
NSQualityOfServiceDefault

eg:Utility 及以下的优先级会受到 iOS9 中低电量模式的控制。另外,在没有用户操作时,90% 任务的优先级都应该在 Utility 之下。

NSBlockOperation

NSOperation的子类,管理一个或多个块的并发执行的操作。

概观

NSBlockOperation类是NSOperation的一个具体子类,它管理一个或多个块的并发执行。可以使用此对象一次执行多个块,而不必为每个块创建单独的操作对象。当执行多个块时,只有当所有块都完成执行时,才认为操作本身已经完成。

添加到操作中的块(block)将以默认优先级分配到适当的工作队列。

方法属性

管理操作中的块

// 创建并返回一个NSBlockOperation对象,并添加指定的块到该对象中。
+ (instancetype)blockOperationWithBlock:(void (^)(void))block;
// 将指定的块添加到要执行的块列表中。
- (void)addExecutionBlock:(void (^)(void))block;
// 与接收器关联的块。
@property (readonly, copy) NSArray<void (^)(void)> *executionBlocks;

NSInvocationOperation

NSOperation的子类,管理作为调用指定的单个封装任务执行的操作。

概观

NSInvocationOperation类是NSOperation的一个具体子类,可以使用它来初始化一个包含在指定对象上调用选择器的操作。这个类实现了一个非并发操作。

方法属性

初始化

// 返回一个用指定的目标和选择器初始化的NSInvocationOperation对象。
- (instancetype)initWithTarget:(id)target selector:(SEL)sel object:(nullable id)arg;
// 返回用指定的调用对象初始化的NSInvocationOperation对象。
- (instancetype)initWithInvocation:(NSInvocation *)inv NS_DESIGNATED_INITIALIZER;

获取属性

// 接收者的调用对象。
@property (readonly, retain) NSInvocation *invocation;
// 调用或方法的结果
@property (nullable, readonly, retain) id result;

常量

// 如果调用result方法时出现错误,则由NSInvocationOperation引发的异常名称。
Result Exceptions

NSOperationQueue

管理操作执行的队列

概观

NSObject子类。操作队列根据其优先级和就绪程度执行其排队的NSOperation对象。在添加到操作队列后,操作将保持在其队列中,直到它报告其任务结束为止。在队列被添加后,您不能直接从队列中删除操作。

提示:
操作队列保留操作直到完成,队列本身保留到所有操作完成。使用未完成的操作挂起操作队列可能导致内存泄漏。

确定执行顺序

队列中的操作是根据它们的状态、优先级和依赖关系来组织的,并相应地执行。如果所有排队的操作都具有相同的queuePriority并准备好在放入队列时执行(也就是说,它们的就绪属性返回yes),那么它们将按照提交到队列的顺序执行。否则,操作队列总是执行优先级最高的操作。

但是不应该依赖队列语义来确保操作的特定执行顺序,因为操作准备状态的更改可能会更改最终的执行顺序。操作间依赖关系为操作提供了绝对的执行顺序,即使这些操作位于不同的操作队列中。一个操作对象直到它的所有依赖操作都完成执行后才被认为准备好执行。

取消操作

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

提示
取消操作会导致操作忽略它可能具有的依赖项。这种行为使队列能够尽快执行操作的start方法。开始方法依次将操作移动到结束状态,以便可以将其从队列中删除。

KVO兼容属性

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

  • operations - 只读
  • operationCount - 只读
  • maxConcurrentOperationCount - 读写
  • suspended - 读写
  • name - 读写

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

线程安全

从多个线程中使用一个NSOperationQueue对象是安全的,无需创建额外的锁来同步对该对象的访问。

操作队列使用调度框架来启动其操作的执行。因此,操作总是在单独的线程上执行,而不管它们是被指定为同步的还是异步的。

属性&方法

访问特定操作队列

// 返回与主线程关联的操作队列。缺省总是有一个queue。
@property (class, readonly, strong) NSOperationQueue *mainQueue;
// 返回启动当前操作的操作队列。
@property (class, readonly, strong, nullable) NSOperationQueue *currentQueue;

管理队列中的操作

// 将指定的操作添加到接收器。
- (void)addOperation:(NSOperation *)op;
//将指定的操作添加到队列。
- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait;
// 在操作中包装指定的块并将其添加到接收器。
- (void)addOperationWithBlock:(void (^)(void))block;
// 当前在队列中的操作。
@property (readonly, copy) NSArray<__kindof NSOperation *> *operations;
// 队列中当前的操作数。
@property (readonly) NSUInteger operationCount;
// 取消所有排队和执行的操作。
- (void)cancelAllOperations;
// 阻塞当前线程,直到所有接收者的排队操作和执行操作完成为止
- (void)waitUntilAllOperationsAreFinished;

管理操作的执行

// 应用于使用队列执行的操作的默认服务级别。
@property NSQualityOfService qualityOfService;
// 可以同时执行的队列操作的最大数量。
@property NSInteger maxConcurrentOperationCount;
// 在队列中并发执行的默认最大操作数。
NSOperationQueueDefaultMaxConcurrentOperationCount

暂停执行

// 一个布尔值,表示队列是否在主动调度要执行的操作。(suspended 挂起,暂停的)
@property (getter=isSuspended) BOOL suspended;

当该属性的值为NO时,队列将积极启动队列中已准备执行的操作。将此属性设置为YES时,可以防止队列启动任何排队着的操作,但是已经执行的操作将继续执行。可以继续将操作添加到已挂起的队列中,但在将此属性更改为NO之前,这些操作不会安排执行。
操作只有在结束执行后才从队列中删除。但是,为了结束执行,必须首先启动一个操作。因为挂起的队列不会启动任何新操作,所以它不会删除当前排队但未执行的任何操作(包括已取消的操作)。

可以使用键值观察监视此属性值的更改。配置一个观察者来监视操作队列的suspended键路径。
此属性的默认值是NO。

队列配置

// 操作队列名称
@property (nullable, copy) NSString *name;
// 用于执行操作的调度队列。
@property (nullable, assign /* actually retain */) dispatch_queue_t underlyingQueue;

四、使用

1. NSInvocationOperation

创建:调用Start方法开启。默认情况下,调用start方法不会开辟一个新线程去执行操作,而是在当前线程同步执行操作。

创建方式一:使用initWithInvocation方法,可以设置0个或多个参数

NSMethodSignature *sig = [[self class] instanceMethodSignatureForSelector:@selector(addSig:)];
NSInvocation *invo = [NSInvocation invocationWithMethodSignature:sig];
NSString * info = @"NSMethodSignature";
[invo setTarget:self];
[invo setSelector:@selector(addSig:)];
 //argumentLocation 指定参数,以指针方式
 // idx 参数索引,第一个参数的起始index是2,因为index为1,2的分别是self和selector
[invo setArgument:(__bridge void *)(info) atIndex:2];
NSInvocationOperation *invocationOp = [[NSInvocationOperation alloc] initWithInvocation:invo];
[invocationOp start];

创建方式二:使用initWithTarget

// 初始化
NSInvocationOperation *invocationOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationOpSel:) object:@"111"]; // 操作的第一个
// 执行
[invocationOp start];

2. NSBlockOperation

创建第一个操作任务,一般不会开辟新线程,就在当前线程中执行。之后的任务都是开辟新线程。执行异步任务。

创建方式一:使用init:创建操作对象,然后使用addExecutionBlock:添加执行

NSBlockOperation * op1 = [[NSBlockOperation alloc] init];
 [op1 addExecutionBlock:^{
     NSLog(@"1 beign");
     NSLog(@"1--%@",[NSThread currentThread]);
     NSLog(@"1 end");
 }];
 [op addExecutionBlock:^{
     NSLog(@"2 beign");
     NSLog(@"2--%@,currentQueue >>>> %@",[NSThread currentThread],[NSOperationQueue currentQueue]);
     NSLog(@"2 end");
 }];

 [op addExecutionBlock:^{
     NSLog(@"3 beign");
     NSLog(@"3--%@,currentQueue >>>> %@",[NSThread currentThread],[NSOperationQueue currentQueue]);
     NSLog(@"3 end");
 }];
 [op1 start];

创建方式二:使用blockOperationWithBlock创建操作对象

NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
     NSLog(@"1 beign");
     NSLog(@"1--%@,currentQueue >>>> %@",[NSThread currentThread],[NSOperationQueue currentQueue]); // 第一个操作任务,一般不会开辟新线程。就在当前线程中执行
     NSLog(@"1 end");
 }];
 // 以下操作任务,会开辟新线程
 [op addExecutionBlock:^{
     NSLog(@"2 beign");
     NSLog(@"2--%@,currentQueue >>>> %@",[NSThread currentThread],[NSOperationQueue currentQueue]);
     NSLog(@"2 end");
 }];

 [op addExecutionBlock:^{
     NSLog(@"3 beign");
     NSLog(@"3--%@,currentQueue >>>> %@",[NSThread currentThread],[NSOperationQueue currentQueue]);
     NSLog(@"3 end");
 }];

 [op start];

3. NSOperationQueue

3.1. 将操作对象添加到队列中

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *blockOp = [NSBlockOperation blockOperationWithBlock:^{
   NSLog(@"1 beign");
   NSLog(@"1--%@",[NSThread currentThread]);
   NSLog(@"1 end");
}];
[queue addOperation:blockOp];

3.2. 添加依赖

直接使用start启动一个操作对象而非将操作对象添加到NSOperationQueue对象中是没有意义的。因为当给操作对象发送start消息后,启动操作,如果线程未阻塞会立即执行该任务。所以就没有所谓的执行顺序。只有将操作对象添加到NSOperationQueue对象中,在队列调度的时候,可以按照依赖、优先级等因素顺序的调度任务。

注意:一定要在添加线程对象NSOperationQueue之前,进行依赖设置。否则依赖将无法达到预期效果。

a. 通队列之间的依赖

// 创建队列
 NSOperationQueue *queue = [[NSOperationQueue alloc] init];
 // 创建操作
 NSInvocationOperation *invocationOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationOpSel:) object:@"invocationOp--arg"];

 NSInvocationOperation *invocationOp2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationOp2Sel:) object:@"invocationOp2--arg"];
 // 设置依赖,操作invocationOp2的任务执行完,才会执行操作invocationOp的任务。
 [invocationOp addDependency:invocationOp2];
 // 执行
 [queue addOperation:invocationOp];
 [queue addOperation:invocationOp2];

b. 不同队列间的依赖

// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 创建操作
NSBlockOperation *block1Op = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"block1Op -- begin");
    [NSThread sleepForTimeInterval:3]; // 模拟耗时操作
    NSLog(@"block1Op -- end");
}];
NSBlockOperation *block2Op = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"block2Op -- begin");
    [NSThread sleepForTimeInterval:4]; // 模拟耗时操作
    NSLog(@"block2Op -- end");
}];

// 创建队列
NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
// 创建操作
NSBlockOperation *block3Op = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"block3Op -- begin");
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"block3Op -- end");
}];
NSBlockOperation *block4Op = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"block4Op -- begin");
    [NSThread sleepForTimeInterval:1]; // 模拟耗时操作
    NSLog(@"block4Op -- end");
}];

// 设置依赖,操作invocationOp2的任务执行完,才会执行操作invocationOp的任务。
[block1Op addDependency:block3Op];
[block3Op addDependency:block2Op];

// block2Op --> block3Op --> block1Op
// 添加操作到队列中
[queue addOperation:block1Op];
[queue addOperation:block2Op];
[queue2 addOperation:block3Op];
[queue2 addOperation:block4Op];

从上代码可以得到block1Op、block2Op、block3Op三个操作的执行顺序:block2Op --> block3Op --> block1Op。

// 创建操作
NSBlockOperation *blockOp = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"blockOp");
    // 模拟耗时操作
    [NSThread sleepForTimeInterval:3];
}];
NSBlockOperation *block2Op = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"block2Op -- begin");
    // 等blockOp操作对象的任务执行完,才能接着往下执行
    [blockOp waitUntilFinished];
    NSLog(@"block2Op --end");
}];
// 执行
[queue addOperation:blockOp];
[queue addOperation:block2Op];

3.3. 获取属性获取主队列

NSOperationQueue *queue = [NSOperationQueue mainQueue];

3.4. 获取属性获取当前队列

NSOperationQueue *queue = [NSOperationQueue currentQueue];

3.5. 进度修改:NSOperationQueue队列的暂停、继续和取消。

// 初始化队列
- (NSOperationQueue *)manualQueue{
    if (!_manualQueue) {
         _manualQueue = [NSOperationQueue new];
        _manualQueue.maxConcurrentOperationCount = 2;
    }
    return _manualQueue;
}

NSBlockOperation *blockOperation1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"1--start");
        [NSThread sleepForTimeInterval:3];
        NSLog(@"1--end");
    }];

    NSBlockOperation *blockOperation2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"2--start");
        [NSThread sleepForTimeInterval:1];
        NSLog(@"2--end");
    }];

    NSBlockOperation *blockOperation3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"3--start");
        [NSThread sleepForTimeInterval:4];
        NSLog(@"3--end");
    }];

    NSBlockOperation *blockOperation4 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"4--start");
        [NSThread sleepForTimeInterval:3];
        NSLog(@"4--end");
    }];


    [self.manualQueue addOperation:blockOperation1];
    [self.manualQueue addOperation:blockOperation2];
    [self.manualQueue addOperation:blockOperation3];
    [self.manualQueue addOperation:blockOperation4];

a. 暂停

如果任务正在执行将不会受到影响。因为任务已经被队列调度到一个线程上并执行。当NSOperationQueue对象属性suspended设置为YES,是队列停止了对任务调度。对那些还在线程中的操作有影响的。

self.manualQueue.suspended = YES;

b. 继续

队列将积极启动队列中已准备执行的操作。

self.manualQueue.suspended = NO;

c. 取消

对于队列中的操作,只有操作标记为已结束才能被队列移除。

  1. 在队列中未被调度的操作,会调用start方法执行操作,以便操作对象处理取消事件。然后标记这些操作对象为已结束。
  2. 对于正在线程中执行其任务的操作对象,正在执行的任务会继续执行,该操作对象会被标记经结束。
[self.manualQueue cancelAllOperations];

3.6. 操作完成

a. 监听操作完成

可以在操作执行完成后,添加额外的内容。使用属性completionBlock,可以为NSOperation对象的任务完成后添加额外的操作。但是不可在completionBlock中追加任务,因为操作(operation)已经启动执行或者结束后不可以添加block任务。

NSBlockOperation *blockOperation1 = [NSBlockOperation blockOperationWithBlock:^{
  // 添加的任务
}];
blockOperation1.completionBlock = ^{
  // 添加额外的内容
};
[blockOperation1 start];

b. 监听操作完成
当执行到某个操作对象发送了一个消息waitUntilFinished:消息。当前线程会被阻塞,之前发送消息的操作对象的任务执行完毕。当前线程才会被唤起,进入准备状态,开始执行相应的任务。

// 创建队列
 NSOperationQueue *queue = [[NSOperationQueue alloc] init];
 // 创建操作
 NSBlockOperation *blockOp = [NSBlockOperation blockOperationWithBlock:^{
     [NSThread sleepForTimeInterval:3]; // 模拟耗时操作
 }];
 NSBlockOperation *block2Op = [NSBlockOperation blockOperationWithBlock:^{
     NSLog(@"block2Op -- begin");
     [blockOp waitUntilFinished]; // 等blockOp操作对象的任务执行完,才能接着往下执行
     NSLog(@"block2Op --end");
 }];
 // 执行
 [queue addOperation:blockOp];
 [queue addOperation:block2Op];

3.7. 最大并发量

NSOperationQueue是并发队列,maxConcurrentOperationCount表示最大的并发数。
当maxConcurrentOperationCount是1时,虽然NSOperationQueue对象是并发,但是线程依然是同步执行任务。但是和GCD同步不同的是,依赖和优先级因素会影响NSOperationQueue对象调度任务的顺序。添加任务顺序不一定是执行顺序。

// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 创建操作
NSBlockOperation *block1Op = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"block1Op -- begin");
    [NSThread sleepForTimeInterval:3]; // 模拟耗时操作
    NSLog(@"block1Op -- end");
}];
NSBlockOperation *block2Op = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"block2Op -- begin");
    [NSThread sleepForTimeInterval:4]; // 模拟耗时操作
    NSLog(@"block2Op -- end");
}];
queue.maxConcurrentOperationCount = 1; // 最大并发个数
[block1Op addDependency:block2Op];// 添加依赖
//    block2Op.queuePriority  = NSOperationQueuePriorityHigh ;
[queue addOperation:block1Op];
[queue addOperation:block2Op];

五、自定义NSOperation子类


我们可以定义串行和并发的2种类型的NSOperation子类。

相关概念

  1. 串行(非并发)的情况
  • 常见使用场景:和网络相关,比如图片下载
  • 使用步骤
    • 实现init方法,初始化操作对象以及一些其他对象
    • 重写main方法,在里面实现想要执行的方法
    • 在main方法中,创建自动释放池,因为如果是异步操作,无法访问主线程的自动释放池
    • 经常通过cancelled属性检查方法是否取消,并且对取消的做出响应
  • 响应取消事件
    • 取消事件可以在任何时间发生
    • 定期调用对象的isCancelled方法,如果返回“YES”,则立即返回,不再执行任务
      isCancelled方法本身非常轻量级,可以频繁调用,没有任何显着的性能损失
  • 位置调用
    • 在执行任何实际工作之前
    • 在循环的每次迭代期间或者如果每次迭代相对较长,较频繁时至少调用一次
    • 在代码中相对容易中止操作的任何点
  1. 并发
  • 重写方法
    • 必需重写四个方法:start、asynchronous、executing、finished
    • start(必需):所有并发操作必须重写此方法,并需要使用自定义的实现替换默认行为。任何时候都不能调用父类的start方法。 即不可使用super。重写的start方法负责以异步的方式启动一个操作,无论是开启一个线程还是调用异步函数,都可以在start方法中进行。注意在开始操作之前,应该在start中更新操作的执行状态,因为要给KVO的键路径发送当前操作的执行状态,方便查看操作状态。
    • main(可选):在这个方法中,放置执行给定任务所需的代码。应该定义一个自定义初始化方法,以便更容易创建自定义类的实例。当如果定义了自定义的getter和setter方法,必须确保这些方法可以从多个线程安全地调用。虽然可以在start方法中执行任务,但使用此方法实现任务可以更清晰地分离设置和任务代码,即在start方法中调用mian方法。注意:要定义独立的自动释放池与别的线程区分开。
    • isFinished(必需):表示是否已完成。需要实现KVO通知机制。
    • isAsynchronous(必需):默认返回 NO ,表示非并发执行。并发执行需要自定义并且返回 YES。后面会根据这个返回值来决定是否并发。
    • isExecuting(必需):表示是否执行中,需要实现KVO通知机制。
  1. KVO兼容
      NSOperation类键路径实现KVO观察有:
    isCancelled、isConcurrent、isExecuting、isFinished、isReady、dependencies、queuePriority、completionBlock

如果重写start方法或对除重写 main之外的执行其他重要的NSOperation对象,则必须确保自定义对象仍然符合这些关键路径的KVO。当重写start方法时,一定需注意sExecuting和isFinished键路径。如果要支持某些依赖项,还可以重写isReady方法,直到满足自定义依赖项前强制它返回NO。 (如果实现自定义依赖关系,还想支持NSOperation类提供的默认依赖项管理系统,请确保在isReady方法中调用super)当操作对象的就绪状态更改时,为isReady键路径生成KVO通知以报告这些变化。除非重写addDependency:或removeDependency:方法,否则不需要担心为依赖关键路径生成KVO通知。虽然可以为NSOperation的其他键路径生成KVO通知,但是没需要这样做。如果需要取消操作,可以简单地调用现有的cancel方法。类似地,几乎不需要修改操作对象中的队列优先级信息。最后,除非您的操作能够动态更改其并发状态,否则不需要为isConcurrent关键路径提供KVO通知。
  自己创建自动释放池,异步操作无法访问主线程的自动释放池

使用

实现例子如下:
非并发的情况下需要重写main方法,并且最好添加一个init方法用于初始化数据。

+ (instancetype)downloaderOperationWithURLPath:(NSString *)urlPath completeBlock:(CompleteBlock)completeBlock{
    WNNoCurrentOPration *op = [[WNNoCurrentOPration alloc] init];
    op.urlPath = urlPath;
    op.completeBlock  = completeBlock;
    return op;
}
// main一般只适合自定义非并发的,在里面实现想执行的任务
- (void)main{
    // 是异步的话 就会导致访问不到当前的释放池
    @autoreleasepool {
        NSLog(@"%s",__func__);
        // 当处于取消操作,不执行任务功能
        if (self.isCancelled) return;
        // 下载图片的耗时操作
        NSURL *url = [NSURL URLWithString:self.urlPath];
        NSData *data = [NSData dataWithContentsOfURL:url];
        NSLog(@"已下载 %@",[NSThread currentThread]);
        UIImage *image = [UIImage imageWithData:data];
        // 主线程回调,完成操作后通知调用方完成回调
        dispatch_async(dispatch_get_main_queue(), ^{
            if (self.completeBlock != nil) {
                self.completeBlock(image);

            }
        });
    }
}

六、GCD VS NSOperation

GCD是苹果公司为多核的并行运算提出的解决方案,会自动利用更多的CPU内核(比如双核、四核),而NSOperation是基于GCD的面向对象的封装,拥有GCD的特性。GCD是将任务(block)添加到队列(串行/并行/全局/主队列),并且以同步/异步的方式执行任务的函数,而NSOperation将操作(一般是异步的任务)添加到队列(一般是并发队列),就会执行指定操作的函数。
 相对于NSThread或者是跨平台的pthread而言,GCD和NSOperation都是自动管理线程的生命周期,开发者只要专注于具体任务逻辑,不需要编写任何线程管理相关的代码。
  GCD提供了一些NSOperation不具备的功能:延迟执行、一次性执行、调度组;NSOperation里提供了一些方便的操作:最大并发数、 队列的暂定/继续、取消所有的操作、指定操作之间的依赖关系(GCD可以用同步实现功能);
  GCD是无法控制线程的最大并发数的,而NSOperation可以设置最大并发数,可以灵活的根据需要限制线程的个数。因为开辟线程需要消耗必要的资源。

何时使用GCD:
调度队列(Dispatch queues)、分组(groups)、信号量(semaphores)、栅栏(barriers)组成了一组基本的并发原语。对于一次性执行,或者简单地加快现有方法的速度,使用轻量级的GCD分派(dispatch)比使用NSOperation更方便。

何时使用NSOperation:
在特定队列优先级和服务质量(用于表示工作对系统的性质和重要性)下, 可以用一系列依赖来调度NSOperation对象 。与在GCD队列上调度的block不同,NSOperation可以被取消和查询其操作状态。通过子类化,NSOperation可以关联执行结果,以供之后参考。

注意:NSOperation和GCD不是互斥的。

七、队列VS线程VS任务

从思维导图了解整个概况。

1. 队列(queue)

队列是先进先出特征数据结构。并且队列只是负责任务的调度,而不负责任务的执行。
按照任务的调度方式可以分为串行队列和并发队列。特点总结如下:

  • 串行队列
    • 一个接一个的调度任务
    • 无论队列中所指定的执行任务是同步还是异步,都会等待前一个任务执行完成后,再调度后面的任务。
  • 并发队列
    • 可以同时调度多个任务。
    • 如果当前调度的任务是同步执行的,会等待任务执行完成后,再调度后续的任务。
    • 如果当前调度的任务是异步执行的,同时底层线程池有可用的线程资源,会再新的线程调度后续任务的执行。

我们知道系统提供了2个队列:主队列和全局并发队列两种队列。我们还可以自己创建队列。

  • 主队列
    • 特点
      • 添加到主队列中的任务,都会在主线程中执行。
      • 专门用来在主线程上调度任务的队列。
      • 在主线程空闲时才会调度队列中的任务在主线程执行。
      • 不会开启线程。
      • 串行。
    • 获取
      • 会随着程序启动一起创建。
      • 主队列只需要获取不用创建。
      • 主队列是负责在主线程调度任务的。
  • 全局队列
    • 本质是一个并发队列,由系统提供,方便编程,可以不用创建就直接使用。
    • 全局队列是所有应用程序共享的
    • GCD的一种队列
    • 全局队列没有名字,但是并发队列有名字。有名字可以便于查看系统日志
  • 自定义队列
    • 有2种方式:串行、并发。
    • 添加到自定义队列中的任务,都会自动放在子线程中执行。

2. 线程(thread)

  • 开辟线程具有一定的资源开销,iOS系统下主要成本包括:内核数据结构(大约1KB)、栈空间(子线程512KB、主线程1MB,也可以使用-setStackSize:设置,但必须是4K的倍数,而且最小是16K),创建线程大约需要90毫秒
  • 对于单核CPU,同一时间CPU只能处理1条线程,即只有1条线程在执行,多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换),如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。
  • 线程是CPU调度和分派且能独立运行基本单位。
  • 线程执行任务,实际做事的功能单元。
  • 异步:开辟新线程。

3. 任务(task)

一定要分清队列、线程和任务这三者的关系:队列调度任务,将任务添加对应的线程上,然后任务是在线程中执行。
任务的执行分为同步和异步。

  • 同步
    • 当前任务未完成,不会执行下个任务
    • 不具备开辟新线程能力
  • 异步
    • 当前任务未完成,同样可以执行下一个任务
    • 具备开辟新线程能力,但是不一定会开辟线程。开辟线程需要CPU等资源,而系统资源有限。不可能开辟无限个线程。

推荐博客