iOS中常见的实现多线程并发的有三种方式,NSThread,NSOperation和GCD。Operation Queues实现并发的主要方式是通过NSOperation&NSOperationQueue实现,主要分以下三步,本文的主要结构也大致是如下结构。
- 实例化NSOperation子类,绑定执行操作
- 创建NSOperationQueue,将NSOperation实例添加进来
- 系统自动将NSOperationQueue队列中检测取出和执行NSOperation操作
NSOperation
NSOperation实例我们称之为操作对象,操作对象可以将需要执行的代码和相关数据集成并封装。操作对象通常不直接执行,而是它加入到操作队列中按顺序调用。同时操作对象也可以直接调用start方法来执行任务,但为了能并行执行任务,标准做法是在start内创建线程。
NSOperation本身是抽象基类,因此必须使用它的子类,使用NSOperation子类的方式有2种:
- 使用系统提供的两个具体子类: NSInvocationOperation和NSBlockOperation
- 自定义子类继承NSOperation,实现内部相应的方法
NSInvocationOperation
通过object & selector 非常方便地创建一个NSInvocationOperation,我们已经有了一个现成的方法,而方法
NSInvocationOperation *invocationOperation=[[NSInvocationOperation alloc]initWithTarget:self selector:@selector(run) object:nil];
NSBlockOperation
使用子类NSBlockOperation,通过使用NSBlockOperation来执行一个或多个block,只有当一个NSBlockOperation所关联的所有block都执行完毕时候,这个NSBlockOperation才算完成,有点类似于dispatch_group概念。从上面打印结果看到在多个线程执行任务。addExecutionBlock:可以为NSBlockOperation添加额外的操作。如果当前NSOperation的任务只有一个的话,那肯定不会开辟一个新的线程,只能同步执行。只有NSOperation的任务数>1的时候,这些额外的操作才有可能在其他线程并发执行。
- (void)runInvocationOp{
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@", [NSThread currentThread]);
}];
for (int i = 0; i < 5; i++) {
[op addExecutionBlock:^{
NSLog(@"%d------%@", i,[NSThread currentThread]);
}];
}
[op start];
}
执行结果如下
2017-05-10 18:40:04.642 JQMultiThread[19102:1441939] ------{number = 1, name = main}
2017-05-10 18:40:04.643 JQMultiThread[19102:1441939] 1------{number = 1, name = main}
2017-05-10 18:40:04.643 JQMultiThread[19102:1441939] 2------{number = 1, name = main}
2017-05-10 18:40:04.644 JQMultiThread[19102:1441939] 4------{number = 1, name = main}
2017-05-10 18:40:04.644 JQMultiThread[19102:1442083] 3------{number = 3, name = (null)}
2017-05-10 18:40:04.644 JQMultiThread[19102:1442071] 0------{number = 4, name = (null)}
自定义NSOperation
通常来说,我们都是通过将operation添加到operation queue来执行operation的,但这并不是必须的,可以通过start
方法执行一个operation,但是这种方式是无法保证异步执行的。当系统定义的两个子类NSInvocationOperation和NSBlockOperation不能很好的满足我们的需求时,我们可以自动移自己的NSOperation类,我们可以定义非并发和并发两种不同类型的NSOperation子类,定义非并发要比并发简单得多。
系统预定义的两个子类 NSInvocationOperation 和 NSBlockOperation 不能很好的满足我们的需求时,我们可以自定义自己的 NSOperation 子类,添加我们想要的功能。我们可以自定义非并发和并发两种不同类型的 NSOperation 子类,而自定义一个前者要比后者简单得多。
- 定义继承自NSOperation的子类,通过实现内部相应的方法来创建任务。
非并发的NSOperation子类
最低限度来说,实现非并发NSOperation子类需要实现两个初始化方法
- 自定义初始化方法
- main方法
引用官方文档的例子如下
- (id)initWithURL:(NSURL *)url scanCount:(NSInteger)scanCount
{
self = [super init];
if (self)
{
self.loadURL = url;
ourScanCount = scanCount;
}
return self;
}
// -------------------------------------------------------------------------------
// main:
//
// Examine the given file (from the NSURL "loadURL") to see it its an image file.
// If an image file examine further and report its file attributes.
//
// We could use NSFileManager, but to be on the safe side we will use the
// File Manager APIs to get the file attributes.
// -------------------------------------------------------------------------------
-(void)main {
if (![self isCancelled])
{
// test to see if it's an image file
if ([self isImageFile:loadURL])
{
// in this example, we just get the file's info (mod date, file size) and report it to the table view
//
NSNumber *fileSize;
[self.loadURL getResourceValue:&fileSize forKey:NSURLFileSizeKey error:nil];
NSDate *fileCreationDate;
[self.loadURL getResourceValue:&fileCreationDate forKey:NSURLCreationDateKey error:nil];
NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
[formatter setTimeStyle:NSDateFormatterNoStyle];
[formatter setDateStyle:NSDateFormatterShortStyle];
NSString *modDateStr = [formatter stringFromDate:fileCreationDate];
NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys:
[self.loadURL lastPathComponent], kNameKey,
[self.loadURL absoluteString], kPathKey,
modDateStr, kModifiedKey,
[NSString stringWithFormat:@"%ld", [fileSize integerValue]], kSizeKey,
[NSNumber numberWithInteger:ourScanCount], kScanCountKey, // pass back to check if user cancelled/started a new scan
nil];
if (![self isCancelled])
{
// for the purposes of this sample, we're just going to post the information
// out there and let whoever might be interested receive it (in our case its MyWindowController).
//
[[NSNotificationCenter defaultCenter] postNotificationName:kLoadImageDidFinish object:nil userInfo:info];
}
}
}
}
并发的NSOperation子类
在默认情况下,operation 是同步执行的,也就是说在调用它的 start 方法的线程中执行它们的任务。而在 operation 和 operation queue 结合使用时,operation queue 可以为非并发的 operation 提供线程,因此,大部分的 operation 仍然可以异步执行。但是,如果你想要手动地执行一个 operation ,又想这个 operation 能够异步执行的话,你需要做一些额外的配置来让你的 operation 支持并发执行。下面列举了一些你可能需要重写的方法:
start
:(Required) ,所有并发执行的 operation 都必须要重写这个方法,替换掉 NSOperation 类中的默认实现。start 方法是一个 operation 的起点,我们可以在这里配置任务执行的线程或者一些其它的执行环境。另外,需要特别注意的是,在我们重写的 start 方法中一定不要调用父类的实现;
main
:(Optional) ,通常这个方法就是专门用来实现与该 operation 相关联的任务的。尽管我们可以直接在 start 方法中执行我们的任务,但是用 main 方法来实现我们的任务可以使设置代码和任务代码得到分离,从而使 operation 的结构更清晰;
isExecuting
和 isFinished
:(Required) ,并发执行的 operation 需要负责配置它们的执行环境,并且对外报告执行环境的状态。因此,一个并发执行的 operation 必须要维护一些状态信息,用来记录它的任务是否正在执行,是否已经完成执行等。此外,当这两个方法所代表的值发生变化时,我们需要生成相应的 KVO 通知,以便外界能够观察到这些状态的变化;
isConcurrent
:(Required) ,这个方法用来标识一个 operation 是否是并发的 operation ,需要重写这个方法并返回 YES 。
@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
- (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"];
//start方法重写之后,苹果不会再主动执行main方法
[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
executing = YES;
[self didChangeValueForKey:@"isExecuting"];
}
- (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"];
}
NSOperation生命周期与状态
NSOperation被创建后有几个生命周期,Pending,Ready,Executing,Finished,Cancel状态,前三个状态都可以直接执行cancel的操作。
operation开始执行之后,会一直执行任务直到完成,或者显式地取消操作。取消可能发生在任何时候,甚至在operation执行之前。尽管NSOperation提供了一个方法,让应用取消一个操作,但是识别出取消事件则是我们自己的事情。若operation直接终止, 可能无法回收所有已分配的内存或资源。因此operation对象需要检测取消事件,并优雅地退出执行。NSOperation对象需要定期地调用isCancelled方法检测操作是否已经被取消,如果返回YES(表示已取消),则立即退出执行。不管是自定义NSOperation子类,还是使用系统提供的两个具体子类,都需要支持取消。isCancelled方法本身非常轻量,可以频繁地调用而不产生大的性能损失
以下地方可能需要调用isCancelled:
- 在执行任何实际的工作之前
- 在循环的每次迭代过程中,如果每个迭代相对较长可能需要调用多次
- 代码中相对比较容易中止操作的任何地方
需要注意的是,为了让我们自定义的operation能够取消操作,我们需要在代码中定期检查isCancelled方法的返回值,一旦检查到这个方法返回YES,就需要立刻停止执行接下来任务。
如果是进行特定任务比如数据请求或者数据下载,我们可以采用系统重写cancel的方法取消操作,但可能会出现一种情况,就是在检查的过程中,这个操作完成了,一旦进入finished状态后,就cancel不掉了,因为没有这个路径,直接从finished到cancel。AFNetworking和SDWebImage的cancel操作都是直接重写,然后去取消下载或者请求的操作。
AFNetworking 中AFURLConnectionOperation的cancel实现
- (void)cancel {
[self.lock lock];
if (![self isFinished] && ![self isCancelled]) {
[super cancel];
if ([self isExecuting]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
}
[self.lock unlock];
}
SDWebImage中SDWebImageDownloaderOperation的cancel实现
- (void)cancel {
@synchronized (self) {
if (self.thread) {
[self performSelector:@selector(cancelInternalAndStop) onThread:self.thread withObject:nil waitUntilDone:NO];
}
else {
[self cancelInternal];
}
}
}
- (void)cancelInternalAndStop {
if (self.isFinished) return;
[self cancelInternal];
CFRunLoopStop(CFRunLoopGetCurrent());
}
- (void)cancelInternal {
if (self.isFinished) return;
[super cancel];
if (self.cancelBlock) self.cancelBlock();
if (self.connection) {
[self.connection cancel];
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
});
// As we cancelled the connection, its callback won't be called and thus won't
// maintain the isFinished and isExecuting flags.
if (self.isExecuting) self.executing = NO;
if (!self.isFinished) self.finished = YES;
}
[self reset];
}
NSOperation 属性的KVO维护
通过观察以下Key paths
,可以非常容易地被观察到NSOperation的状态:
- isCancelled
- isConcurrent
- isExecuting
- isFinished
- isReady
- dependencies
- queuePriority
- completionBlock
如果你重写了start方法,或者对除了main函数外的NSOperation进行了重大定制,你必须确保自定义对象保持符合这些关键路径的KVO,当覆盖start
方法的时候,最应该关注的key patshs变化是isExecuting和isFinished,因为这两个key paths
最容易受到重写start方法的影响
NSOperationQueue
一个NSOperation对象可以通过调用start方法来执行任务,默认是同步执行的。也可以将NSOperation添加到一个NSOperationQueue(操作队列)中去执行,而且是异步执行的。具体的执行不用我们自己去管理,都由操作系统去处理。NSOperationQueue和GCD中的并发队列、串行队列略有不同的是:NSOperationQueue一共有两种队列:主队列(maxConcurrentOperationCount为1)、其他队列。其中其他队列同时包含了串行、并发功能。
主队列:凡是加到主队列中的operation,都会放到主线程执行。
NSOperationQueue *queue = [NSOperationQueue mainQueue];
其他队列:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
将任务加到队列中去
把任务加入到队列中主要是以下方法
- (void)addOperation:
,添加一个 operation 到 operation queue 中。
- (void)addOperations:(NSArray
,添加一组 operation 到 operation queue 中。
- (void)addOperationWithBlock:
,直接添加一个 block 到 operation queue 中,而不用创建一个 NSBlockOperation 对象。
- (void)runAddOperation
{
NSOperationQueue *_queue = [[NSOperationQueue alloc] init];
NSBlockOperation *_blockOp = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"block operation");
}];
NSBlockOperation *_copyOp = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"Copy Operation");
}];
NSInvocationOperation *_invocationOp =[[NSInvocationOperation alloc] initWithTarget:self
selector:@selector(runInvocationOp)
object:nil];
[_queue addOperationWithBlock:^{
NSLog(@"add block");
}];
[_queue addOperation:_invocationOp];
[_queue addOperations:@[_blockOp, _copyOp] waitUntilFinished:NO];
}
- (void)runInvocationOp
{
NSLog(@"send Log");
}
设置最大并发数(maxConcurrentOperationCount)
NSOperationQueue类是设计用来执行操作的并行执行,但你可以强行把maxConcurrentOperationCount设置成1用来执行串行操作。但这个队列的执行顺序并不跟GCD中的串行队列那样完全遵循FIFO,而是由好几个因素决定,比如operation 的 isReady 状态,优先级等。如果不设置maxConcurrentOperationCount的属性,那么它的默认值就是
NSOperationQueueDefaultMaxConcurrentOperationCount,就是系统可设置最大操作并行数。
设置操作的任务依赖
通过配置依赖关系,我们可以让不同的operation串行之行,一个operation只有在它依赖的所有operation都执行完成之后才能开始执行。配置operation的依赖关系要涉及到NSOperation类中的addDependency方法。注意三点
* 依赖是单向的操作,[A addDependency:B],表示A依赖B,但B并不会依赖A,不然会形成循环依赖
* 依赖也并不局限在同一个queue中
* 一定不能形成循环依赖,否则会形成死锁
Operation的优先级
当加入到一个队列中,执行顺序取决于operation的isReady状态以及它们对应的优先级。isReady状态取决于操作之间的相互依赖。但是操作优先级水平则是完全由operation的优先级属性决定。所有新建的操作都有一个默认的normal优先级,但是你可以通过setQueuePriority方法来人为增加或者降低操作优先级。
Operation Queue vs Grand Central Dispatch(GCD)
苹果的文档中已经对两者的差别和使用场景给出了官方的解释
GCD is a low-level API that gives you the flexibility to structure your code in a variety of different ways. In contrast, NSOperation provides you with a default structure that you can use for your asynchronous code. If you're looking for an existing, well-defined structure that's perfectly tailored for Cocoa applications, use NSOperation. If you're looking to create your own structure that exactly matches your problem space, use GCD.
GCD
是基于C语言开发的底层API,可以灵活地以各种不同的方式构建代码。相比之下NSOperation给你提供了可异步处理代码的默认结构。如果你正在寻找一种完美适用于Cocoa应用程序的结构请使用NSOperation。如果你希望创建与您的问题完全匹配的自己的结构,可以使用GCD。即可以归结为两点,GCD更底层,而NSOperation则为我们带来了一些面向对象的封装,也带来了方便的灵活性。我自己对两者的归结点如下:
-
NSOperation
: 建立在 GCD 的基础之上的,面向对象的解决方案,当operation需要添加相互依赖,或者取消一个正在执行的operation,暂停或者恢复operation queue时候,NSOperation无疑更加灵活。 -
GCD
:基于C层级的API,轻量级,FIFO执行并发的方式,使用GCD时候我们并不关心任务的调度情况,而让系统帮我们自动处理。
Simple and Reliable Threading with NSOperation
Concurrency Programming Guide
iOS 并发编程之 Operation Queues