操作队列(Operation Queues)

Cocoa操作是封装你想异步执行的任务的一个面向对象的方法。操作被设计成跟操作队列结合使用或者它们自己使用。由于它们基于Objective-C,所以操作通常用在OS X和iOS中基于Cocoa的应用程序。

这个章节讲述如何定义以及使用操作。

关于操作对象(About Operation Objects)

一个操作对象是你用来封装你想让应用程序执行的工作的NSOperation类的实例。NSOperation类自己是一个抽象基类,必须子类化以实现有用的工作。尽管是抽象的,该类也提供了相当数量的基础设施以减少你在你的子类里必须做的工作。另外,基础框架提供了两个具体子类,你可以像你的现有代码一样使用。(the Foundation framework provides two concrete subclasses that you can use as-is with your existing code.)表格2-1列出了这些类,以及你如何使用每一个的概述。

表格2-1 框架中的操作类

Class Description
NSInvocationOperation 你使用的类是基于应用程序中的对象和选择器创建操作对象。你可以在有一个已有的方法已经执行了需要的任务的情况下使用。由于它不需要子类化,你也可以使用该类以更动态的方式创建操作对象。更多关于如何使用该类的信息,参阅Creating an NSInvocationOperation Object
NSBlockOperation 并行执行一个或更多块对象的类。因为它可以执行不止一个块,所以块操作对象使用组语义进行操作;只有当所有相关的块已经完成执行时,操作本身才算完成。更多关于如何使用该类的信息,参阅Creating an NSBlockOperation Object。该类在OS X v10.6及以后可用。更多关于块的信息,参阅Blocks Programming Topics
NSOperation 定义自定义操作对象的基类。子类化NSOperation通过实现你自己的操作给你完全的控制,包括替换操作执行的默认方法以及报告它的状态。更多关于如何定义自定义的操作对象,参阅Defining a Custom Operation Object

所有操作对象都支持下列关键特性:

操作被设计用来帮组你提升你的应用程序的并发级别。操作也是组织以及把应用程序的行为封装成简单的离散块的好方法。代替在应用程序的主线程运行一些代码,你可以提交一个或更多操作对象到一个队列,并让相关的工作异步地在一个或多个独立的线程执行。

并发与非并发操作(Concurrent Versus Non-concurrent Operations)

尽管你通常通过添加它们到一个操作队列来执行操作,但这不是必须的。通过调用start方法手动执行一个操作对象也是可以的,但是这样做不能保证操作跟你剩下的代码并行运行。NSOperation类的isConcurrent方法告诉你是否一个操作像对于它的start方法调用的那个线程是同步还是异步运行。默认这个方法返回NO,这意味着操作在调用线程里同步运行。

如果你向实现一个并行操作--一个相对于调用线程异步运行的--你必须写额外的代码异步启动该操作。例如,你可能产生一个独立的线程,调用一个异步系统函数或执行任何其他操作以来确保start方法启动任务并立即返回,并且很可能在任务完成前返回。

大部分开发者可能永远不需要实现并行操作对象。如果你总是添加你的操作到一个操作队列,你确实不需要实现并行操作。当你提交一个非并行操作到一个操作队列,队列自己会创建一个线程供你的操作运行。因此,添加一个非并行操作到操作队列仍然导致你的操作对象代码的异步执行。只有在需要异步执行操作而不添加到操作队列的情况下,才需要定义并发操作的功能。

关于创建并发操作的信息,参阅Configuring Operations for Concurrent ExecutionNSOperation Class Reference

创建一个NSInvocationOperation对象(Creating an NSInvocationOperation Object)

NSInvocationOperation是NSOperation类的一个具体子类,运行时调用你指定的对象中的指定的selector。使用这个类可以避免为应用程序中的每个任务定义大量的自定义操作;尤其如果你在修改一个现有的应用程序并且已经有执行必要任务所需的对象和方法。当你想调用的方法可以根据情况改变依赖时你也可以使用该类。例如,你可以使用调用操作执行一个基于用户输入动态选择的选择器。

创建一个调用操作的过程是直截了当的。创建并初始化类的一个新实例,然后传递渴望的对象和选择器给初始化方法执行。清单2-1展示了一个自定义类中演示了创建过程的两个方法。taskWithData:方法创建了一个新调用对象并提交给它另外一个包含任务实现的方法的名称。

清单2-1 创建一个NSInvocationOperation对象

@implementation MyCustomClass
- (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对象(Creating an NSBlockOperation Object)

NSBlockOperation类是NSOperation类的一个具体子类,充当一个或多个块对象的包装。这个类为已经使用操作队列并且也不想创建调度队列的应用程序提供面向对象的包装器。你也可以使用块操作来利用操作依赖、KVO通知以及调度队列可能没有的其他特性。

创建块操作时,初始化时你通常添加至少要一个块;你可以以后根据需要添加更多块。当执行一个NSBlockOperation对象时,该对象提交所有它的块到默认优先级的并发调度队列。然后对象等待所有块完成执行。最有一个块完成执行时,操作对象标记自己为完成。因此,你可以使用块操作跟踪一组执行中的块,就像你可能使用线程链接来合并多个线程的结果一样。不同点是因为块操作自己运行在一个独立的线程,应用程序中的其他线程在等待块操作完成时可以继续工作。

清单2-2展示了如何创建一个NSBlockOperation对象的简单例子。块自己没有参数并且没有有意义的返回结果。

清单2-2 创建一个NSBlockOperation对象

NSBlockOperation* theOp = [NSBlockOperation blockOperationWithBlock: ^{
      NSLog(@"Beginning operation.\n");
      // Do some work.
   }];

创建块操作对象之后,你可以使用addExecutionBlock:方法添加更多块给它。如果你需要串行执行这些块,你必须直接将它们提交到所需的调度队列。

定义一个自定义操作对象(Defining a Custom Operation Object)

如果块操作和调用操作对象不能满足你的应用程序的需求,你可以直接子类NSOperation并添加任何你需要的行为。NSOperation类为所有操作对象提供通用的分类点。该类也提供了大量的基础设施以处理依赖以及KVO通知所需的大部分工作。然而,你可能仍然要补充现有基础架构,以确保你的操作正确无误。你必须做的额外工作的量取决于你是实现一个非并发操作还是并发操作。

定义一个非并发操作比定义并发操作更简单。对于非并发操作,所有你需要做的是执行主要任务并正确地响应取消事件;现有类基础框架为你做了其余的所有工作。对于并发操作,你必须用你自定义的代码代替一些现有基础框架。下列部分展示了如何实现这两种类型的对象。

执行主要任务(Performing the Main Task)

最低限度,每一个操作对象应该至少实现下面的方法:

  • 一个自定义初始化方法
  • main

你需要一个自定义初始化方法将你的操作对象置于已知的状态,一个自定义main方法执行任务。当然,你可以向下面一样按照需求实现额外的方法:

  • 自定义你打算在你main方法实现中调用的方法
  • 用于设置数据值以及访问操作结果的访问器方法
  • NSCoding协议方法允许你归档和反归档操作对象

清单2-3展示了自定义NSOperation子类的起始模板。(这个清单没有展示如何处理取消,但是展示了你通常会有的方法。关于处理取消的信息,参阅Responding to Cancellation Events。)该类的初始化方法有一个单独的对象作为参数,并且操作对象内保存了它的一个引用。在返回结果给应用程序之前main方法表面上处理该数据对象。

清单 2-3 定义一个简单的操作对象

@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

响应取消事件(Responding to Cancellation Events)

操作开始执行后,它会持续执行直到完成或者直到你的代码明确地取消该操作。取消可以发生在任何时间,甚至在操作开始执行前。尽管NSOperation类为客户端提供了取消操作的方法,但认识取消事件是必要的自愿行为。如果一个操作被彻底终止,可能没有方法可以回收已经分配的资源。因此,操作对象应该检查取消事件,并在操作中间发生时正常退出。

在一个操作对象中支持取消,所有你需要做的是在你自定义的代码中周期性地调用对象的isCancelled方法,如果它返回YES立即返回。无论你的操作持续时间如何,或者你是直接子类化NSOperation还是使用其中一个具体的子类,支持取消都很重要。isCancelled方法自己是非常轻量的,并且可以频繁调用而没有任何显著的性能损失。当定义你的操作对象时,你应该考虑在你的代码中的下列地方调用isCancelled方法:

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

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

清单 2-4 响应取消请求

- (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.
   }
}

尽管上面的例子没有清理的代码,你自己的代码应该保证释放由你的自定义代码分配的任务资源。

为并行执行配置操作(Configuring Operations for Concurrent Execution)

操作对象默认以同步方式运行--也就是说,它们在调用其启动(start)方法的线程上执行其任务。因为操作队列为非并行操作提供线程,但大部分操作依然异步运行。然而,如果你打算手动执行操作并且依然希望它们异步运行,你必须采取适当的操作以确保它们可以运行。你可以通过定义你的操作为并发操作来实现。

表格 2-2 列出了实现一个并发操作通常重写的方法。

表格 2-2 并发操作重写的方法

Method Description
start (必须)所有并发操作都必须重写这个方法并用它们自己的自定义实现代替默认行为。手动执行一个操作,调用它的start方法。因此,你的这个方法的实现是操作的启动点并且是你设置线程或其他执行任务的执行环境的位置。你的实现在任何时候都不能调用super方法。
main (可选)这个方法通常用来实现操作对象相关的任务。尽管你可以在start方法中执行任务,但实现这个方法可以把设置和任务代码更清晰地分离。
isExecuting、isFinished (必须)并发操作负责设置它们的执行环境并且对外部客户报告该环境的状态。因此,并行操作必须保持一些状态信息来知道什么时候它在执行任务以及什么时候它完成了任务。它必须用这些方法报告该状态。你的这些方法的实现必须可以安全的从其他线程同步调用。更改这些方法报告的值时,你还必须为预期的关键路径生成适当的KVO通知。
isConcurrent (必须)辨认一个操作是并发操作,重写这个方法并返回YES。

这个章节的剩下部分展示了MyOperation类的示例实现,它示范操作了实现一个并行操作所需的基础代码。MyOperation类在它创建一个单独线程中执行它自己的的main方法。main方法执行的实际工作是不相关的。样例示范操作了定义一个并发操作你需要提供的基础设施。

清单 2-5 展示了MyOperation类的接口和部分实现。MyOperation类中isConcurrent、isExecuting和isFinished方法的实现是相对直接了当的。isConcurrent方法应该简单地返回YES以说明这是一个并发操作。isExecuting和isFinished方法简单地返回存在类自己的实例变量中的值。

清单2-5 定义并发操作

@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

清单2-6展示了MyOperation的start方法。该方法的实现很少,以便演示了你必须执行的任务。这种情况下,该方法简单地启动一个新线程并配置它调用main方法。该方法也更新了executing成员变量并为关键地址生成了KVO通知以反映该值大的改变。它的工作完成后,该方法简单地返回,让新分离的线程执行实际的任务。

清单2-6 start方法

- (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"];
}

清单2-7展示了MyOperation类剩余的实现。就像在清单2-6中看到的,main方法是新线程的入口。它执行操作对象相关的工作,当工作最后完成时调用自定义的completeOperation方法。completeOperation方法然后为isExecuting和isFinished关键路径都生成了需要的KVO通知以反映操作状态的改变。

清单2-7 完成时更新操作

- (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合规性(Maintaining KVO Compliance)

NSOperation类对一下关键路径是符合键值观察(KVO):

  • isCancelled
  • isConcurrent
  • isExecuting
  • isFinished
  • isReady
  • dependencies
  • queuePriority
  • completeBlock

如果你重写了start方法或者对NSOperation对象做了重写main之外任何重要的自定义,你必须确保你的自定义对象为这些关键路径保持KVO兼容。重写start方法时,你最需要考虑的关键路径是isExecutingisFinished。这些是通过重新实现该方法最常受影响的关键路径。

如果你想实现对其他操作对象之外的东西的依赖支持,你也可以重写isReady方法并强制它返回NO知道你的定制依赖满足。(如果你实现定制依赖,如果你依旧支持NSOperation类提供的默认依赖管理系统,确保从你的isReady方法中调用super。)当你的操作对象的准备状态改变时,为isReady关键路径生成KVO通知以报告这些更改。除非你重写addDependency:removeDependency:方法,你不需要担心为dependencies关键路径生成KVO通知。

尽管你可以为NSOperation的其他关键路径生成KVO通知,这是不可能的,你永远不需要这样做。如果你需要取消一个操作,你可以简单地调用现有的取消方法。类似的,很少有需要在操作对象中修改队列优先级信息。最后,除非你的操作能够动态修改它的并发状态,你不需要为isConcurrent关键路径提供KVO通知。

更多关于键值观察以及如何在你的自定义对象中支持它的信息,参阅Key-Value Observing Programming Guide

定制一个操作对象的执行行为(Customizing the Execution Behavior of an Operation Object)

操作对象的配置发生在你创建它们之后但是添加它们到队列之前。这个章节描述的配置类型可以应用于所有的操作对象,不管你自己子类NSOperation或使用现有的子类。

配置互操作相关性(Configuring Interoperation Dependencies)

依赖关系是序列化不同操作对象执行的方法。一个依赖其他操作的操作不能开始执行直到所有它依赖的操作全部完成执行。因此,你可以使用在两个操作对象间依赖创建简单的一对一依赖或者创建复杂的对象依赖图。

在两个操作对象间建立依赖关系,使用NSOperationaddDependency:方法。这个方法创建了从当前操作对象到你指定作为参数的目标对象的单方向的依赖。该依赖意味着当前对象不能开始执行直到目标对象完成执行。依赖关系不局限于相同队列中的操作。操作对象管理它们自己的依赖关系,因此创建操作间依赖并添加它们到不同的队列是完全可以接受的。然而,有件不能接受的事是创建操作间的循环依赖。这样做事一个程序员错误,会阻止受影响的操作执行。

当一个操作的所有依赖它们自己完成执行,操作对象通常准备好执行。(如果你定制了isReady方法的行为,操作的准备状态取决于你设置的标准。)如果操作对象在队列中,队列随时可能启动执行该操作。如果你计划手动执行该操作,你需要调用操作的启动方法。

重要:你应该总是在运行操作或添加它们到操作队列前配置依赖。后来添加的依赖可能无法阻止操作对象的运行。

依赖关系依赖于当对象的状态改变时每个操作对象发出合适的KVO通知。如果你定制了操作对象的行为,你可能需要从你的自定义代码中生成合适的KVO通知,目的是避免导致依赖关系的问题。更多关于KVO通知以及操作对象的信息,参阅Maintaining KVO Compliance。关于配置依赖关系的额外信息,参阅NSOperation Class Reference

改变一个操作的执行优先级(Changing an Operation’s Execution Priority)

对于添加到队列的操作,执行顺序首先取决于排队操作的准备然后是它们的相对优先级。准备就绪取决于操作对其他操作的依赖,但优先级级别是操作对象自己的一个属性。默认,所有新的操作对象有一个默认优先级,但你可以根据需要通过调用对象的setQueuePriority:方法提升或降低该优先级。

优先级级别仅适用于在相同操作队列中的操作。如果你的应用程序有多个操作队列,则每个操作队列都独立于其他队列的优先级。因此,在不同的队列中低优先级操作在高优先级操作前执行是可能的。

优先级不是依赖关系的替代。优先级决定操作队列开始仅执行当前准备好的操作的顺序。例如,如果一个队列包含一个高优先级和一个低优先级操作,并且两个操作都准备就绪,该队列先执行高优先级操作。然而,如果高优先级操作没有准备就绪但是低优先级操作就绪了,队列限制性低优先级操作。如果你想阻止一个操作启动直到另外一个操作完成,你必须使用依赖关系(如Configuring Interoperation Dependencies中描述的一样)。

改变底层线程优先级(Changing the Underlying Thread Priority)

OS X v10.6及以后,配置操作的底层线程的执行优先级是可以的。系统中的线程原则是通过内核自己管理,但普遍高优先级线程被给予比低优先级线程更多的运行机会。在操作对象中,将线程优先级指定为0.0到1.0范围内的浮点值,0.0是最低优先级而1.0是最高优先级。如果你不指定一个明确的线程优先级,操作以默认线程优先级0.5运行。

设置操作的线程优先级,你必须在添加它到队列之前(或手动执行前)调用操作对象的setThreadPriority:方法。当执行操作时,默认启动方法使用你指定的值修改当前线程的优先级。新的优先级只在操作的mian方法执行期间保持有效。所有其他代码(包括操作的完成块)在默认线程优先级上运行。如果你创建一个并行操作,并因此重写了start方法,你必须自己配置线程优先级。

设置完成块(Setting Up a Completion Block)

OS X v10.6及以后,操作可以在它的主要任务完成之后时执行一个完成块。你可以使用完成块执行任何你不认为是主要任务的工作。例如,你可以使用该块通知感兴趣的客户操作已经完成。并发操作对象可以使用该块生成它的最终KVO通知。

设置完成块,使用NSOperationsetCompletionBlock:方法。你传递给这个方法的块应该没有参数也没有返回值。

实现操作对象的技巧(Tips for Implementing Operation Objects)

尽管操作兑现实现起来相当简单,但写你的代码时还是有些事情需要注意。下列章节描述为你的操作对象写代码时需要考虑到的因素。

操作对象中管理内存(Managing Memory in Operation Objects)

下列章节描述了操作对象中好的内存管理的关键因素。关于Objective-C编程中内存管理的一般信息,参阅Advanced Memory Management Programming Guide

避免单线程存储(Avoid Per-Thread Storage)

尽管大部分操作在线程中执行,非并发操作情况下,线程通常由操作队列提供。如果操作对垒为你提供了一个线程,你应该考虑线程由队列拥有,而不能让操作接触。尤其,你永远都不应该关联任何数据到不是你自己创建和管理的线程上。操作队列管理的线程的来去依赖于系统的需求和你的应用程序。因此,使用每个线程的存储在操作间传递数据是不可靠的且可能会失败。

根据需要保持对操作对象的引用(Keep References to Your Operation Object As Needed)

由于操作对象异步运行,你不应该任务你可以创建它们并忘记它们。它们依然仅仅是对象,而且需要你来管理你的代码需要对它们的任何引用。这是尤其重要的,如果你需要在操作完成后从它取回结果数据的话。

你应该总是保持你自己对操作的引用的原因是你后面可能没有机会从队列获取该对象。队列努力尽可能快地调度并执行操作。许多情况下,队列在操作添加后几乎立即执行它们。在你自己的代码返回到队列中获取操作的引用时,操作可能已经完成并从队列中移除。

处理错误和异常(Handling Errors and Exceptions)

由于操作在应用程序中是分离的不相关的实体,它们有责任处理产生的任何错误或异常。在OS X v10.6及之后,NSOperation类提供的默认启动方法没有捕获异常。(OS X v10.5,启动方法确实捕获并抑制异常。)你自己的代码应该总直接是捕获并抑制异常。应该也检查错误码并按照需要通知应用程序的合适部分。如果你重写了启动方法,你必须在你的自定义实现中立即捕获任何异常以阻止它们离开底层线程的范围。

错误情况的类型中你应该准备处理的是下面这些:

  • 检查并处理UNIX errno样式的错误代码。
  • 检查方法和函数返回的明确的错误码。
  • 捕获你自己的代码或其他系统框架抛出的异常。
  • 捕获NSOperation类自己抛出的异常,下列情况会抛出异常:
    • 当操作没准备好执行但它的启动方法被调用时
    • 当操作正在执行或已完成但它的启动方法又一次被调用时
    • 当你试图添加一个完成块到一个已经在执行的或已经完成的操作时
    • 当你试图检索一个已经被取消的NSInvocationOperation对象时

如果你的自定义代码确实遇到了异常或错误,你应该采取必要的步骤传递错误到应用程序的剩余部分。NSOperation类没有提供传递错误结果代码或异常到应用其他部分的明确方法。因此,如果这些信息对你的应用程序很重要,你必须提供必要的代码。

确定操作对象的合适范围(Determining an Appropriate Scope for Operation Objects)

尽管添加大量的操作到一个操作队列是可能的,但这样做通常是不可能的。像任何对象一样,NSOperation类的实例消耗内存,并且保持它的执行相关的实际的开销。如果每一个你的操作对象只做很少的工作,你创建了成百上千个操作对象,你可能会发现你花费了比执行实际工作更多的时间在调度操作上。并且如果你的应用程序已经是内存受限的,你可能发现仅仅拥有成百上千个操作对象在内存中可能会进一步降低性能。 高效使用操作的关键是在需要做的工作量和保持计算机忙碌之间找到合适的平衡。尝试确保你的操作做一个合理的工作量。例如,如果你的应用程序创建一百个操作对象在一百个不同值上执行同样的任务,考虑创建10个操作对象每个处理10个值。 也应该避免一次添加大量的操作到一个队列,或避免以比队列能处理的更快的速度持续添加操作对象到队列。不要用操作对象淹没队列,而是批量创建这些对象。一个批次完成执行时,使用完成块告知应用程序创建新的一批。当你有很多工作要做时,你想保持队列中充满了足够的操作,以便计算机保持忙碌状态,但你不希望一次创建那么多操作,导致应用程序内存不足。

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

执行操作(Executing Operations)

最终,您的应用程序需要执行操作才能完成相关的工作。 在本节中,你将学习几种执行操作的方法,以及如何在运行时操纵执行操作。

添加操作到操作队列(Adding Operations to an Operation Queue)

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

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

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

[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中串行调度队列完全相同的行为。 如果操作对象的执行顺序对你很重要,那么在将操作添加到队列之前,应该使用依赖关系来建立该顺序。 有关配置依赖关系的信息,请参阅 Configuring Interoperation Dependencies

关于使用操作队列的信息,参阅NSOperationQueue Class Reference。关于串行调度队列的更多信息,参阅Creating Serial Dispatch Queues

手动执行操作(Executing Operations Manually)

虽然操作队列是运行操作对象最方便的方式,但也可以在没有队列的情况下执行操作。 但是,如果您选择手动执行操作,则应该在代码中采取一些预防措施。 特别是,该操作必须准备好运行,并且必须始终使用其启动方法启动它。 在isReady方法返回YES之前,不认为该操作能够运行。 isReady方法集成到NSOperation类的依赖管理系统中,以提供操作依赖关系的状态。 只有当其依赖关系被清除时,才可以开始执行。 手动执行操作时,应始终使用start方法开始执行。 您使用此方法而不是主要方法或其他方法,因为start方法在实际运行自定义代码之前会执行多次安全检查。 特别是,默认启动方法会生成操作正确处理其依赖关系所需的KVO通知。 此方法还可以正确避免执行已经取消的操作(如果它已被取消)以及在操作实际上不准备运行时引发的异常。 如果您的应用程序定义了并发操作对象,则在启动它们之前,还应该考虑调用isConcurrent操作方法。 在此方法返回NO的情况下,您的本地代码可以决定是在当前线程中同步执行操作还是先创建一个单独的线程。 但是,实施这种检查完全取决于您。 清单2-8显示了在手动执行操作之前应该执行的那种检查的简单示例。 如果方法返回NO,则可以安排计时器并稍后再次调用该方法。 然后,您将保持重新计划定时器,直到方法返回YES,这可能是因为操作被取消而发生的。 清单2-8手动执行操作对象

- (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;
}

取消操作(Canceling Operations)

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

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

等待操作完成(Waiting for Operations to Finish)

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

重要说明:您绝不应该在应用程序的主线程中等待操作。 您应该只从辅助线程或其他操作执行此操作。 阻塞您的主线程将阻止您的应用程序响应用户事件,并可能导致您的应用程序反应迟钝。

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

暂停和继续队列(Suspending and Resuming Queues)

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

results matching ""

    No results matching ""