NSOperation和NSOperationQueue

Cocoa operation以一种面向对象的方式来封装需要并发执行的工作,operation被设计为与operation queue一起使用或者单独使用。因为它们基于Objective-C,所以OS X和iOS中基于Cocoa的应用程序最常使用它们。

关于Operation对象

operation对象是NSOperation类的实例,用于封装希望应用程序执行的工作。NSOperation类本身是一个抽象类,为了做任何有用的工作,其必须被分类。尽管是抽象的,但该类确实提供了大量的基础架构来尽量减少我们在自己的子类中要完成的工作量。另外,Foundation框架提供了两个具体的子类,我们可以使用现有代码使用它们。下表列出了这些类以及如何使用每个类的描述。

Class Description
NSInvocationOperation 使用该类基于应用程序中的对象和方法选择器来创建operation对象。当存在一个执行所需任务的方法时,则可以使用此类。因为它不需要子类化,所以可以使用此类以更动态的方式创建operation对象。
NSBlockOperation 使用该类并行执行一个或多个block对象。因为它可以执行多个block,所以block operation对象使用一组语义来操作。只有当所有关联的block已经完成执行时,操作本身才算完成。
NSOperation 该类是用于自定义operation对象的基类。通过子类化NSOperation,我们可以完全控制自己操作的实现,包括更改操作执行的默认方式并报告其状态的功能。

所有operation对象都支持以下主要功能:

  • 支持在operation对象之间建立基于图形的依赖关系。这些依赖关系会阻止给定的操作运行,直到它所依赖的所有操作都已完成运行。
  • 支持可选的完成block,该block在操作的主任务完成后执行(仅限OS X v10.6及更高版本)。
  • 支持使用KVO通知监听对操作执行状态的更改。
  • 支持对操作进行优先级排序,从而影响其相对执行顺序。
  • 支持取消正在执行的操作。

operation旨在帮助提高应用程序中的并发水平。operation也是将应用程序行为组织和封装为简单离散块的好方式。可以将一个或多个operation对象提交给一个队列,并让相应的工作在一个或者多个单独的线程上异步执行,而不是在应用程序的主线程上运行一些代码。

并发与非并发操作

虽然通常通过将操作添加到操作队列来执行操作,但这不是必需的。也可以通过调用操作对象的start方法手动执行操作,但这样做并不能保证该操作与其他代码同时运行。NSOperation类的isConcurrent方法会告知我们一个操作相对于调用start方法的线程是同步还是异步运行的。默认情况下,此方法返回NO,这意味着该操作在调用线程中同步运行。

如果想实现一个并发操作,必须编写额外的代码来异步启动操作。例如,我们可能会创建一个单独的线程,调用异步系统函数或执行其他任何操作来确保start方法启动任务并立即返回,并且很可能在任务完成之前返回。

大多数开发者应该永远不需要实现并发操作对象。如果始终将操作添加到操作队列中,则不需要实现并发操作对象。当向操作队列提交非并发操作时,队列本身会创建一个线程来运行这些操作。因此,操作队列添加非并发操作队列仍然会导致异步执行操作对象代码。只有在需要异步执行操作而不将其添加到操作队列的情况下,才需要定义并发操作的能力。

创建NSInvocationOperation对象

NSInvocationOperation类是NSOperation的具体子类,它在运行时会调用指定的关联对象的方法。使用此类可以避免为应用程序中的每个任务自定义大量的operation对象。特别是如果我们需要修改现有的应用程序并且已经拥有执行必要任务所需的对方和方法。当我们想要调用的方法可以根据具体情况而改变时,可以选择使用该类。例如,可以使用调用操作来执行基于用户输入动态选择的方法选择器。

创建一个NSInvocationOperation对象的过程非常简单。可以创建并初始化类的新实例,将所需的对象和方法选择器传递给初始化方法。以下代码显示了自定义类中的两个方法,用于演示创建过程。taskWithData:方法创建一个新的调用对象并为其提供另一个方法的名称,该方法包含任务的实现。

- (NSOperation*)taskWithData:(id)data
{
    NSInvocationOperation* theOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(myTaskMethod:) object:data];

    return theOp;
}

// This is the method that does the actual work of the task.
- (void)myTaskMethod:(id)data
{
    // Perform the task.
}
@end

创建一个NSBlockOperation对象

NSBlockOperation类是NSOperation的具体子类,充当一个或多个block对象的包装。此类为已经已经使用操作队列并且不想创建调度队列的应用程序提供面向对象的包装器。还可以使用block操作来利用操作依赖关系、KVO 以及可能不适用于调度队列的其他功能。

在创建一个block操作时,通常在初始化时至少添加一个block,并在稍后根据需要添加更多block。当需要执行NSBlockOperation对象时,该操作对象将其所有block对象提交给默认优先级的并发调度队列(concurrent dispatch queue)。操作对象会等待所有block完成执行,当最后一个block完成执行时,操作对象将自身标记为已完成。因此,我们可以使用block操作来跟踪一组正在执行的block,就像使用线程连接合并多个线程的结果一样。区别在于,因为block操作本身在单独的线程上运行,所以应用程序的其他线程可以在等待block操作完成的同时继续工作。

以下代码显示了如何创建一个NSBlockOperation对象的简单示例。该block本身没有参数并且没有返回结果。

NSBlockOperation* theOp = [NSBlockOperation blockOperationWithBlock: ^{

    NSLog(@"Beginning operation.\n");
    // Do some work.
}];

创建block操作对象后,可以使用addExecutionBlock:方法向其添加更多block。如果需要连续执行block,则必须将它们直接提交到所需的调度队列。

定义一个自定义操作对象

如果block操作和invocation操作对象不能完全满足应用程序的需求,则可以直接子类化NSOperation并添加所需的任何行为。NSOperation类为所有操作对象提供了一个通用的继承点。该类还提供了大量的基础架构来处理依赖关系和KVO通知所需的大部分工作。但是,我们可能还需要补充现有的基础架构,以确保我们的操作正确。必须执行的额外工作量取决于我们是在执行非并发还是并发操作。

定义非并发操作比定义并发操作简单得多。对于非并发操作,只需执行主要任务并对取消事件作出对应的响应。现有的类级别的基础架构为我们完成了所有其他工作。对于并发操作,必须用我们的自定义代码替换一些现有的基础架构。以下部分展示了如何实现这两种类型的对象。

执行主要任务

每个操作对象至少应该实现以下方法:

  • 自定义初始化方法。
  • main方法。

我们需要一个自定义的初始化方法来将操作对象设置为已知状态,还需要自定义main方法来执行我们的任务。还可以根据需要实现其他方法,如下所示:

  • 计划在main方法的实现中调用的自定义方法。
  • 用于设置数据值和访问操作结果的访问器方法。
  • 允许我们归档和反归档操作对象的NSCoding协议方法。

以下代码展示了一个自定义NSOperation子类的初始模版。(以下代码并未展示如何处理取消事件,只展示了我们通常会使用的方法。有关如何取消操作的信息,请参看响应取消事件。)此类的初始化方法将单个对象用作数据参数,并存储对操作对象的引用。在将结果返回给应用程序之前,main方法将处理该数据对象。

@interface MyNonConcurrentOperation : NSOperation

@property id (strong) myData;

-(id)initWithData:(id)data;

@end

@implementation MyNonConcurrentOperation

- (id)initWithData:(id)data {
    if (self = [super init])
      myData = data;
    return self;
}

-(void)main {
    @try {
        // Do some work on myData and report the results.
    }
    @catch(...) {
        // Do not rethrow exceptions.
    }
}
@end

有关如何实现NSOperation子类的详细示例,请参看NSOperationSample

响应取消事件

在一个操作开始执行之后,其会执行它的任务直到完成或者我们使用代码明确地取消操作。即使在操作开始执行之前,取消也可能随时发生。尽管NSOperation类为我们提供了一种取消操作的方法,但要意识到取消事件是自愿行为。如果一个操作被彻底终止,可能无法收回已分配的资源。因此,操作对象需要检查取消事件,并在操作过程中正常退出。

为了支持操作对象中的取消操作,只需要定期在自定义代码中调用操作对象的isCancelled方法,并在该方法返回YES时立即执行return操作。无论操作的持续时间如何重要或者是直接子类化NSOperation还是使用其中一个具体的子类,支持取消操作都很重要。isCancelled方法本身非常轻量级,可以频繁调用而不会有任何明显的性能损失。在设计操作对象时,应考虑在代码中以下位置调用isCancelled方法:

  • 在执行任何实际的工作之前。
  • 在循环的每次迭代中至少一次,或者如果每次迭代相对较长,则更频繁。
  • 在代码中相对容易退出操作的任何地方。

以下代码提供了一个非常简单的例子来说明如何在操作对象的main方法中响应取消事件。在这种情况下,每一次while循环都会调用isCancelled方法,允许在工作开始之前快速退出操作并且每隔一段时间再次执行一次。

- (void)main {
    @try {
        BOOL isDone = NO;

        while (![self isCancelled] && !isDone) {
            // Do some work and set isDone to YES when finished
        }
    }
    @catch(...) {
        // Do not rethrow exceptions.
    }
}

虽然以上示例中没有执行清理的代码,但我们在实现时应该确保释放由我们的自定义代码分配的任何资源。

配置操作以支持并发执行

操作对象默认以同步方式执行,也就是说它们在调用其启动方法的线程中执行它们的任务。因为操作队列会为非并发操作对象提供线程,所以大多数操作对象仍然是异步运行的。然而,如果我们计划手动执行操作对象并仍然希望它们异步执行,则必须采取适当的操作以确保它们可以运行。可以通过将操作对象定义为并发操作来完成此操作。

下表列出了通常为了实现并发操作而重写的方法。

Method Description
start (必需)所有并发操作都必须重写此方法,并用它们自己的自定义实现替换默认行为。要手动执行操作,请调用其start方法。因此,此方法的实现是自定义操作的起点,并且是设置执行任务的线程或者其他执行环境的位置。在自定义实现中,不能调用super
main (可选)此方法通常用于实现与操作对象关联的任务。虽然可以在start方法中执行任务,但使用此方法执行任务可以使设置和任务代码更清晰地分离。
isExecuting
isFinished
(必需)并发操作负责设置其执行环境并向外部报告该环境的状态。因此,并发操作必须保存一些状态信息,以知道它如何执行任务以及何时完成该任务。它必须使用这些方法报告该状态。
这些方法的实现必须是线程安全的,以便同时从其他线程调用。更改这些方法报告的值时,还必须按照预期的键路径生成对应的KVO通知。
isConcurrent (必需)要将操作标识为并发操作,请覆写方法并返回YES

本节的余下部分显示了MyOperation类的示例实现,其演示了实现并发操作所需的基本代码。MyOperation类只是在它创建的单独线程上执行自己的main方法。main方法执行的实际工作师无关紧要的。示例的要点是要演示定义并发操作时需要提供的基础架构。

以下代码显示了MyOperation类的接口和部分实现。MyOperation类的isConcurrentisExecutingisFinished方法的实现相对简单。isConcurrent方法应该简单地返回YES来表明这是一个并发操作。isExecutingisFinished方法只是返回存储在类本身的实例变量中的值。

@interface MyOperation : NSOperation {
BOOL        executing;
BOOL        finished;
}
- (void)completeOperation;
@end

@implementation MyOperation
- (id)init {
    self = [super init];
    if (self) {
        executing = NO;
        finished = NO;
    }
    return self;
}

- (BOOL)isConcurrent {
    return YES;
}

- (BOOL)isExecuting {
    return executing;
}

- (BOOL)isFinished {
    return finished;
}
@end

以下代码显示了MyOperation类的start方法。该方法的实现很少,以便演示绝对必须执行的任务。在这种情况下,该方法只需启动一个新线程并配置该线程调用main方法。该方法还更新executing成员变量,并为isExecuting键路径生成KVO通知以反映该值的变化。

- (void)start {
    // Always check for cancellation before launching the task.
    if ([self isCancelled])
    {
        // Must move the operation to the finished state if it is canceled.
        [self willChangeValueForKey:@"isFinished"];
        finished = YES;
        [self didChangeValueForKey:@"isFinished"];
        return;
    }

    // If the operation is not canceled, begin executing the task.
    [self willChangeValueForKey:@"isExecuting"];
    [NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
    executing = YES;
    [self didChangeValueForKey:@"isExecuting"];
}

以下代码显示了MyOperation类的其余实现。如上代码所示,main方法是新线程的入口点。它执行与操作对象关联的任务,并在该任务最终完成时调用自定义completeOperation方法,completeOperation方法然后为isExecutingisFinished键路径生成所需的KVO通知,以反映操作状态的变化。

- (void)main {
    @try {

        // Do the main work of the operation here.

        [self completeOperation];
    }
    @catch(...) {
        // Do not rethrow exceptions.
    }
}

- (void)completeOperation {
    [self willChangeValueForKey:@"isFinished"];
    [self willChangeValueForKey:@"isExecuting"];

    executing = NO;
    finished = YES;

    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isFinished"];
}

即使操作被取消,也应该始终通知KVO观察者操作对象现在已完成其工作。当操作对象依赖于其他操作对象的完成时,它会监听这些对象的isFinished键路径。只有当所有的对象都报告它们已经完成时,才会执行相关的操作信号,表明它已准备好运行。生成完成通知失败,可能会因此阻止应用程序中其他操作的执行。

兼容KVO

NSOperation类兼容了对以下键路径的键-值观察(KVO):

  • isCancelled
  • isConcurrent
  • isExecuting
  • isReady
  • dependencies
  • queuePriority
  • completionBlock

如果覆写start方法或者对NSOperation对象进行除了重写main方法之外的任何重要定制,则必须确保定制对象对这些键路径保持KVO兼容。当覆写start方法时,最应该考虑的键路径应该是isExecutingisFinished,这些是重新实现该方法时最常受影响的键路径。

如果要实现对自定义依赖项(并非其他操作对象)的支持,还可以重写isReady方法,并强制它返回NO,直到满足自定义依赖项为止。(如果要实现自定义依赖项,同时仍然支持由NSOperation类提供的默认依赖项管理系统,请确保在isReady方法调用super。)当操作对象的准备状态更改时,为isReady键路径生成KVO通知报告这些变化。除非重写addDependency:或者removeDependency:方法,否则不需要担心为依赖键路径生成KVO通知。

虽然可以为NSOperation的其他键路径生成KVO通知,但不太可能需要我们这样做。如果需要取消某项操作,则只需要调用现有的cancel方法即可。同样,很少需要修改操作对象中的队列优先级信息。最后,除非操作对象能够动态更改其并发状态,否则不需要为isConcurrent键路径提供KVO通知。

与键-值观察(KVO)有关的更多信息以及如何在自定义对象中支持它的更多信息,请参看Key-Value Observing Programming Guide

定制操作对象的执行行为

操作对象的配置在创建它们之后但在将它们添加到队列之前发生。本节中描述的配置类型可以应用于所有操作对象,无论是使用自定义NSOperation对象还是使用现有的NSOperation子类。

配置相互操作依赖

依赖关系是一种序列化不同操作对象的执行的方式。依赖于其他操作的操作无法开始执行,直到它所依赖的所有操作都已完成执行。因此,可以使用依赖关系来在两个操作对象之间创建简单的一对一依赖关系或构建复杂的对象依赖关系图。

要建立两个操作对象之间的依赖关系,可以使用NSOperation对象的addDependency:方法。此方法创建从当前操作对象到作为参数指定的目标操作对象的单向依赖关系。这种依赖意味着当前对象不能执行,直到目标操作对象完成执行。依赖关系也不限于同一队列中的操作。操作对象管理它们自己的依赖关系,因此在操作之间创建依赖关系并将它们全部添加到不同的队列是完全可以接受的。然而,有一件不可接受的事情是在操作之间创建循环依赖关系。

当一个操作的所有依赖都已经完成时,操作对象通常会准备好执行。(如果自定义isReady方法的行为,则操作的准备就会根据我们设置的条件来确定。)如果操作对象位于队列中,则队列可以随时开始执行该操作。如果打算手动执行操作,则由我们自己来调用操作对象的start方法。

重要:应始终在执行操作或将它们添加到操作队列之前配置依赖关系,之后添加的依赖项可能无法阻止给定的操作对象的执行。

依赖关系依赖于在对象的状态发生改变时每个操作对象发送适当的KVO通知。如果要自定义操作对象的行为,则可能需要在自定义代码中生成对应的KVO通知,以避免导致依赖关系出现问题。

更改一个操作的执行优先级

对于已经添加到队列中的操作,执行顺序首先取决于排队的操作是否准备就绪,然后才取决于相对优先级。是否准备就绪取决于操作对其他操作的依赖性,但优先级是操作对象本身的属性。默认情况下,所有新操作对象都具有“正常”优先级,但可以通过调用操作对象的setQueuePriority:方法来根据需要提高或降低该优先级。

优先级仅适用于在同一操作队列中的操作。 如果应用程序具有多个操作队列,则每个操作队列都独立于其他队列而优先执行自己的操作。因此,低优先级操作仍然可能在不同队列中的高优先级操作之前执行。

优先级并不是依赖关系的替代。优先级决定操作队列开始执行其当前准备就绪的操作的顺序。例如,如果队列中既包含高优先级操作又包含低优先级操作,并且这两个操作都已准备就绪,则队列首先执行高优先级操作。但是,如果高优先级操作未准备就绪,但低优先级操作已准备就绪,则队列首先执行低优先级操作。如果要防止一个操作启动,直到另一个操作完成,则必须使用依赖关系。

更改底层线程优先级

在OS X v10.6及更高版本中,可以配置一个操作的底层线程的执行优先级。系统中的线程策略由内核管理,但通常优先级较高的线程比低优先级的线程有更多的运行机会。在操作对象中,可以将线程优先级指定为0.01.0范围的浮点值,其中0.0是最低优先级,1.0是最高优先级。如果没有指定明确的线程优先级,则该操作对象的默认线程优先级为0.5。

要设置操作对象的线程优先级,必须在将操作对象添加到队列(或手动执行)之前调用其setThreadPriority:方法。当需要执行操作时,默认的start方法会使用指定的值来修改当前线程的优先级。这个新的优先级仅在操作对象的main方法执行期间有效。所有其他代码(包括操作的完成block)都以默认的线程优先级运行。如果创建并发操作,并因此重写start方法,则必须自己配置线程优先级。

配置完成Block

在OS X v10.6及更高版本中,当主任务完成时,操作对象能够执行完成block。可以使用完成block来执行任何我们认为不是主要任务的工作。例如,可以使用完成block通知感兴趣的客户端操作本身已完成。并发操作对象可能会使用此block来生成其最终的KVO通知。

要设置完成block,请使用NSOperation对象的setCompletionBlock:方法,我们传递给此方法的block应该没有参数并且没有返回值。

实现操作对象的一些提示

虽然操作对象很容易实现,但在编写代码时应注意以下几点。以下部分描述了在为操作对象编写代码时应考虑的因素。

管理操作对象中的内存

以下部分描述操作对象中良好内存管理的关键因素。有关Objective-C程序内存关联的信息,请参看Advanced Memory Management Programming Guide

避免Per-Thread存储

尽管大多数操作在一个线程上执行,但在非并发操作的情况下,该线程通常由操作队列提供。如果操作队列为我们提供线程,则应将该线程视为由队列拥有并且不会被操作触及。具体而言,不应该将任何数据与不是由我们自己提供或管理的线程关联。由操作队列管理的线程根据系统和应用程序的需要来来去去。因此,在使用Pre-Thread存储的操作之间传递数据是不可靠的,并且可能会失败。

对于操作对象,在任何情况下都不应该使用Pre-Thread存储。初始化操作对象时,应该为操作对象提供执行其工作所需的所有内容。因此,操作对象本身提供了所需的上下文存储。所有传入和传出的数据都应该存储在那里,直到它可以集成到应用程序中或者不再需要。

根据需要保留对操作对象的引用

不能因为操作对象异步运行,就在创建它们之后将它们完全忘记。它们仍然只是对象,并且由我们自己来管理代码所需的任何对它们的引用。如果需要在操作完成后从操作中检索结果数据,这一点尤其重要。

应该始终保留对操作对象的引用的原因是我们可能没有机会在稍后向队列请求操作对象。队列尽可能快地调度和执行操作。在很多情况下,操作对象被添加到队列后,队列会立即开始执行操作。当我们自己的代码返回到队列以获取对该操作的引用时,该操作可能已经完成并从队列中移除。

错误和异常处理

由于操作本质上是应用程序内的离散实体,因此它们负责处理出现的任何错误或者异常。在OS X v10.6及更高版本中,NSOperation类提供的默认start方法不会捕获异常。(在OS X v10.5中,start方法确实会捕获并抑制异常。)我们自己的代码应该始终直接捕获并抑制异常。它还应该检查错误代码并根据需要通知应用程序的相应部分。如果我们重写了start方法,必须类似地捕获自定义实现中的任何异常,以防止它们离开底层线程的范围。

应该准备处理的错误情况类型包括以下几种:

  • 检查并处理UNIX errno-style错误代码。
  • 检查方法和函数返回的显示错误代码。
  • 捕获我们自己的代码或其他系统框架抛出的异常。
  • 捕获NSOperation类自身抛出的异常,发送以下情况时会抛出异常:
    • 当操作未准备好执行但是调用其start方法时。
    • 当操作正在执行或完成执行(可能是因为它已被取消)并且其start方法再次被调用。
    • 当尝试将完成block添加到正在执行或已完成的操作时。
    • 当尝试检索已取消的NSInvocationOperation对象的结果时。

如果我们的自定义代码确实遇到异常或错误,应该采取任何必要的步骤将错误传播到应用程序的其余部分。NSOperation类不提供将错误结果码或异常传递给应用程序其他部分的显式方法。因此,如果这些信息对应用程序很重要,我们必须提供必要的代码。

确定操作对象的适用范围

尽管能够向操作队列中添加任意大量的操作,但这样做通常是不切实际的。和任何对象一样,NSOperation类的实例消耗内存并且还带来与其执行相关的实际成本。如果每个操作对象执行少量工作,并且创建了数以万计的操作对象,那么我们可能会发现调度操作比开展实际工作会花费更多时间。如果应用程序的内存已经受到内存限制,我们可能会发现在内存中有数以万计的操作对象会进一步降低性能。

有效使用操作的关键是在我们需要做的工作量和保持计算机繁忙之间找到适当的平衡点。尽量确保操作对象执行合理的工作量。例如,如果应用程序创建100个操作对象来在100个不同的值上执行相同的任务,请考虑创建10个操作对象,以便每个操作处理10个值。

还应该避免同时向队列中添加大量操作,或者避免不断将操作对象添加到队列中,而不是将其快速处理。当我们有很多工作要做时,想要让队列中有足够多的操作以便让计算机保持忙碌状态,但是又不希望一次创建那么多操作而导致应用程序内存不足,则应当批量创建操作对象,而不是用操作对象淹没队列。当一个批处理完成执行时,使用完成block告知应用程序创建一个新的批处理。

当然,我们创建的操作对象的数量以及在每个操作中执行的工作量是可变的,并且完全取决于应用程序。应该始终使用诸如Instruments之类的工具来帮助我们在效率和速度之间找到适当的平衡点。有关Instruments以及其他性能工具的概述,请参看Performance Overview

执行操作

将操作添加到操作队列

到目前为止,执行操作的最简单方法是使用操作队列,该操作队列是NSOperationQueue类的一个实例。应用程序负责创建和维护它打算使用的任何操作队列。应用程序可以有任意数量的队列,但是在给定的时间点可以执行的操作数量有实际限制。操作队列与系统一起工作来将并发操作数量限制为适合可用内核和系统负载的值。因此,创建额外的队列并不意味着可以执行其他操作。

以下代码展示了如何创建一个队列:

NSOperationQueue* aQueue = [[NSOperationQueue alloc] init];

要将操作添加到队列中,请使用addOperation:方法。在OS X v10.6及更高版本中,可以使用addOperations:waitUntilFinished:方法添加操作组,也可以使用addOperationWithBlock:方法将block对象直接添加到队列(没有相应的操作对象)。这些方法中的每一个都将一个操作(或多个操作)排队,并通知队列应该开始处理它们。在大多数情况下,操作在被添加到队列后不久就会被立即执行,但操作队列可能由于以下几种原因而延迟排队操作的执行。特别是,如果排队的操作依赖于尚未完成的其他操作,则执行可能会延迟。如果操作队列本身暂停或正在执行的操作数量为其最大并发操作数,则执行也可能会延迟。以下示例显示了将操作添加到队列的基本语法:

[aQueue addOperation:anOp]; // Add a single operation
[aQueue addOperations:anArrayOfOps waitUntilFinished:NO]; // Add multiple operations
[aQueue addOperationWithBlock:^{
    /* Do something. */
}];

重要:应该在将操作对象添加到队列中之前对操作对象进行所有必要的配置和修改,因为一旦添加操作对象,该操作可能立即执行,这对于更改后的预期效果来说可能太迟了。

尽管NSOperationQueue类是为并发执行操作而设计的,但可以强制一个队列一次仅执行一个操作。setMaxConcurrentOperationCount:方法运行我们为操作队列对象配置最大并发操作数量。将值1传递给此方法会使队列一次只执行一个操作。尽管一次只能执行一个操作,但执行顺序仍然基于其他因素,例如每个操作是否准备就绪及其分配的优先级。因此,串行操作队列并不能提供与Grand Central Dispatch中的串行调度队列完全相同的行为。如果操作对象的执行顺序对我们非常重要,那么在将操作添加到队列之前,应该使用依赖关系来建立该顺序。有关配置依赖关系的信息,请参看配置操作之间的依赖关系

有关使用操作队列的信息,请参看NSOperationQueue Class Reference

手动执行操作

虽然操作队列是执行操作对象最方便的方式,但也可以在没有队列的情况下执行操作。但是,如果选择手动执行操作,则应该在代码中采取一些预防措施。特别是,该操作必须已经准备好执行,并且必须始终使用其start方法启动它。

在操作对象的isReady方法返回YES之前,操作对象不能被执行。isReady方法被集成到NSOperation类的依赖管理系统中,以提供操作对象的依赖关系的状态。只有当其依赖关系被清除时,才可以开始执行。

手动执行操作时,应该始终使用start方法来开始执行。使用该方法而不是main方法或者其他方法的原因是因为start方法在实际运行自定义代码之前会执行多个安全检查。特别是,默认的start方法会生成操作对象正确处理其依赖关系所需的KVO通知。该方法还可以正确避免执行操作(如果它已被取消)和在操作实际上未准备就绪时执行而引发异常。

如果应用程序定义了并发操作对象,那么在启动它们之前,还应该考虑调用操作对象的isConcurrent方法。在此方法返回NO的情况下,我们的自定义代码可以决定是在当前线程中同步执行操作还是创建一个单独的线程。

以下代码显示了在手动执行操作之前应该执行的检查的简单示例。如果方法返回NO,则可以安排定时器在稍后再次调用该方法。然后,保持定时器重新定时,直到方法返回为YES(这可能是因为操作被取消而造成的)。

- (BOOL)performOperation:(NSOperation*)anOp
{
    BOOL        ranIt = NO;

    if ([anOp isReady] && ![anOp isCancelled])
    {
        if (![anOp isConcurrent])
            [anOp start];
        else
            [NSThread detachNewThreadSelector:@selector(start) toTarget:anOp withObject:nil];
            
        ranIt = YES;
    }
    else if ([anOp isCancelled])
    {
        // If it was canceled before it was started,
        //  move the operation to the finished state.
        [self willChangeValueForKey:@"isFinished"];
        [self willChangeValueForKey:@"isExecuting"];
        executing = NO;
        finished = YES;
        [self didChangeValueForKey:@"isExecuting"];
        [self didChangeValueForKey:@"isFinished"];

        // Set ranIt to YES to prevent the operation from
        // being passed to this method again in the future.
        ranIt = YES;
    }
    return ranIt;
}

取消操作

一旦操作对象被添加到操作队列中,操作对象实际上由队列拥有并且不能被删除。从操作队列中取出操作的的唯一方法是取消它。可以通过调用单个操作对象的cancel方法来取消它,也可以通过调用操作队列对象的cancelAllOperations方法来取消队列中的所有操作对象。

只有在确定不再需要时才应取消操作。发出取消命令会将操作对象置于“取消”状态,从而阻止其执行。由于取消的操作仍被视为“已完成”,因此依赖于它的操作对象将收到对应的KVO通知以清除该依赖关系。因此,取消所有排队操作来响应某些重大事件(如应用程序退出或用户特别请求取消)比选择性取消操作更为常见。

等待操作完成

为了获得最佳性能,应该将操作设计为尽可能异步,使应用程序在执行操作时可以自由地执行额外的工作。如果创建操作对象的代码也处理该操作对象的结果,则可以使用NSOperationwaitUntilFinished方法来阻拦该代码直到操作完成。但是,一般来说最好避免使用该方法。阻塞当前线程可能是一个方便的解决方案,但它确实会在您的代码中引入更多序列并限制并发执行的操作数量。

重要说明:永远不要等待应用程序主线程中的操作。应该仅在辅助线程或其他操作中这样做。阻塞主线程会导致应用程序无法响应用户事件,并可能导致应用程序显示无响应。

除了等待单个操作完成外,还可以通过调用NSOperationQueuewaitUntilAllOperationsAreFinished方法来等待队列中的所有操作。当等待整个队列完成时,请注意应用程序的其他线程仍可以将操作添加到队列中,从而延长等待时间。

暂停和恢复队列

如果想要暂时停止执行操作,则可以使用setSuspended:方法挂起响应的操作队列。暂停队列不会导致已执行的操作在其任务执行期间暂停。它只是阻止新的操作被安排执行。我们可能会暂停队列以响应用户请求暂停任何正在进行的工作,因为期望用户可能最终想要恢复该工作。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容