深入学习:NSOperationQueue, NSRunLoop和线程安全

来源:互联网 发布:淘宝近视眼镜店 编辑:程序博客网 时间:2024/04/29 17:05

深入学习:NSOperationQueue, NSRunLoop和线程安全

目前在 iOS 和 OS X 中有两套先进的同步 API 可供我们使用:NSOperation 和 GCD 。其中 GCD 是基于 C 的底层的 API ,而 NSOperation 则是 GCD 实现的 Objective-C API。 虽然 NSOperation 是基于 GCD 实现的, 但是并不意味着它是一个 GCD 的 “dumbed-down” 版本, 相反,我们可以用NSOperation 轻易的实现一些 GCD 要写大量代码的事情。 因此, NSOperationQueue 是被推荐使用的, 除非你遇到了 NSOperationQueue 不能实现的问题。

1. 为什么优先使用NSOperationQueue而不是GCD

曾经我有一段时间我非常喜欢使用GCD来进行并发编程,因为虽然它是C的api,但是使用起来却非常简单和方便, 不过这样也就容易使开发者忘记并发编程中的许多注意事项和陷阱。
比如你可能写过类似这样的代码(这样来请求网络数据):

dispatch_async(_Queue, ^{  //请求数据  NSData *data = [NSData dataWithContentURL:[NSURL URLWithString:@"http://domain.com/a.png"]];    dispatch_async(dispatch_get_main_queue(), ^{         [self refreshViews:data];    });});

没错,它是可以正常的工作,但是有个致命的问题:这个任务是无法取消的 dataWithContentURL:是同步的拉取数据,它会一直阻塞线程直到完成请求,如果是遇到了超时的情况,它在这个时间内会一直占有这个线程;在这个期间并发队列就需要为其他任务新建线程,这样可能导致性能下降等问题。
因此我们不推荐这种写法来从网络拉取数据。
操作队列(operation queue)是由 GCD 提供的一个队列模型的 Cocoa 抽象。GCD 提供了更加底层的控制,而操作队列则在 GCD 之上实现了一些方便的功能,这些功能对于 app 的开发者来说通常是最好最安全的选择。NSOperationQueue相对于GCD来说有以下优点:

  • 提供了在 GCD 中不那么容易复制的有用特性。
  • 可以很方便的取消一个NSOperation的执行。
  • 可以更容易的添加任务的依赖关系
  • 提供了任务的状态:isExecuteing, isFinished.

2. Operation Queues的使用

2.1 NSOperationQueue

NSOperationQueue 有两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。在两种类型中,这些队列所处理的任务都使用 NSOperation 的子类来表述。

NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];  //主队列NSOperationQueue *queue = [[NSOperationQueue alloc] init]; //自定义队列NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{                //任务执行            }];[queue addOperation:operation];

我们可以通过设置 maxConcurrentOperationCount 属性来控制并发任务的数量,当设置为 1 时, 那么它就是一个串行队列。主对列默认是串行队列,这一点和 dispatch_queue_t 是相似的。
2.2 NSOperation
你可以使用系统提供的一些现成的 NSOperation 的子类, 如 NSBlockOperation、 NSInvocationOperation 等(如上例子)。你也可以实现自己的子类, 通过重写 main 或者 start 方法 来定义自己的 operations 。
使用 main 方法非常简单,开发者不需要管理一些状态属性(例如 isExecuting 和 isFinished),当 main 方法返回的时候,这个 operation 就结束了。这种方式使用起来非常简单,但是灵活性相对重写 start 来说要少一些, 因为main方法执行完就认为operation结束了,所以一般可以用来执行同步任务。
1
2
3
4
5
6
@implementation YourOperation
- (void)main
{
// 任务代码 …
}
@end
如果你希望拥有更多的控制权,或者想在一个操作中可以执行异步任务,那么就重写 start 方法, 但是注意:这种情况下,你必须手动管理操作的状态, 只有当发送 isFinished 的 KVO 消息时,才认为是 operation 结束
1
2
3
4
5
6
7
8
9
10
11
12
@implementation YourOperation
- (void)start
{
self.isExecuting = YES;
// 任务代码 …
}
- (void)finish //异步回调
{
self.isExecuting = NO;
self.isFinished = YES;
}
@end
当实现了start方法时,默认会执行start方法,而不执行main方法
为了让操作队列能够捕获到操作的改变,需要将状态的属性以配合 KVO 的方式进行实现。如果你不使用它们默认的 setter 来进行设置的话,你就需要在合适的时候发送合适的 KVO 消息。
需要手动管理的状态有:
isExecuting 代表任务正在执行中
isFinished 代表任务已经执行完成
isCancelled 代表任务已经取消执行
手动的发送 KVO 消息, 通知状态更改如下 :
1
2
3
[self willChangeValueForKey:@”isCancelled”];
_isCancelled = YES;
[self didChangeValueForKey:@”isCancelled”];
为了能使用操作队列所提供的取消功能,你需要在长时间操作中时不时地检查 isCancelled 属性, 比如在一个长的循环中:
1
2
3
4
5
6
7
8
9
@implementation MyOperation

  • (void)main
    {
    while (notDone && !self.isCancelled) {
    // 任务处理
    }
    }
    @end
    1. RunLoop
      在cocoa中讲到多线程,那么就不得不讲到RunLoop。 在ios/mac的编码中,我们似乎不需要过多关心代码是如何执行的,一切仿佛那么自然。比如我们知道当滑动手势时,tableView就会滚动,启动一个NSTimer之后,timer的方法就会定时执行, 但是为什么呢,其实是RunLoop在帮我们做这些事情:分发消息。
      3.1 什么是RunLoop
      你应该看过这样的伪代码解释ios的app中main函数做的事情:
      1
      2
      3
      4
      5
      6
      int main(int argc, char * argv[])
      {
      while (true) {
      [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
      }
      }
      也应该看过这样的代码用来阻塞一个线程:
      1
      2
      3
      while (!complete) {
      [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
      }
      或许你感觉到他们有些神奇,希望我的解释能让你明白一些.
      我们先思考一个问题: 当我们打开一个IOS应用之后,什么也不做,这时候看起来是没有代码在执行的,为什么应用没有退出呢?
      我们在写c的简单的只有一个main函数的程序时就知道,当main的代码执行完,没有事情可做的时候,程序就执行完毕退出了。而我们IOS的应用是如何做到在没有事情做的时候维持应用的运行的呢? 那就是RunLoop。
      RunLoop的字面意思就是“运行回路”,听起来像是一个循环。实际它就是一个循环,它在循环监听着事件源,把消息分发给线程来执行。RunLoop并不是线程,也不是并发机制,但是它在线程中的作用至关重要,它提供了一种异步执行代码的机制。
      3.2 事件源
      runloop
      由图中可以看出NSRunLoop只处理两种源:输入源、时间源。而输入源又可以分为:NSPort、自定义源、performSelector:OnThread:delay:, 下面简单介绍下这几种源:
      3.2.1 NSPort 基于端口的源
      Cocoa和 Core Foundation 为使用端口相关的对象和函数创建的基于端口的源提供了内在支持。Cocoa中你从不需要直接创建输入源。你只需要简单的创建端口对象,并使用NSPort的方法将端口对象加入到run loop。端口对象会处理创建以及配置输入源。
      NSPort一般分三种: NSMessagePort(基本废弃)、NSMachPort、 NSSocketPort。 系统中的NSURLConnection就是基于NSSocketPort进行通信的,所以当在后台线程中使用NSURLConnection 时,需要手动启动RunLoop, 因为后台线程中的RunLoop默认是没有启动的,后面会讲到。
      3.2.2 自定义输入源
      在Core Foundation程序中,必须使用CFRunLoopSourceRef类型相关的函数来创建自定义输入源,接着使用回调函数来配置输入源。Core Fundation会在恰当的时候调用回调函数,处理输入事件以及清理源。常见的触摸、滚动事件等就是该类源,由系统内部实现。
      一般我们不会使用该种源,第三种情况已经满足我们的需求
      3.2.3 performSelector:OnThread
      Cocoa提供了可以在任一线程执行函数(perform selector)的输入源。和基于端口的源一样,perform selector请求会在目标线程上序列化,减缓许多在单个线程上容易引起的同步问题。而和基于端口的源不同的是,perform selector执行完后会自动清除出run loop。
      此方法简单实用,使用也更广泛。
      3.2.4 定时源
      定时源就是NSTimer了,定时源在预设的时间点同步地传递消息。因为Timer是基于RunLoop的,也就决定了它不是实时的。
      3.3 RunLoop观察者
      我们可以通过创建CFRunLoopObserverRef对象来检测RunLoop的工作状态,它可以检测RunLoop的以下几种事件:
      Run loop入口
      Run loop将要开始定时
      Run loop将要处理输入源
      Run loop将要休眠
      Run loop被唤醒但又在执行唤醒事件前
      Run loop终止
      3.4 Run Loop Modes
      RunLoop对于上述四种事件源的监视,可以通过设置模式来决定监视哪些源。 RunLoop只会处理与当前模式相关联的源,未与当前模式关联的源则处于暂停状态。
      cocoa和Core Foundation预先定义了一些模式(Apple文档翻译):
      Mode Name Description
      Default NSDefaultRunLoopMode (Cocoa) kCFRunLoopDefaultMode (Core Foundation) 缺省情况下,将包含所有操作,并且大多数情况下都会使用此模式
      Connection NSConnectionReplyMode (Cocoa) 此模式用于处理NSConnection的回调事件
      Modal NSModalPanelRunLoopMode (Cocoa) 模态模式,此模式下,RunLoop只对处理模态相关事件
      Event Tracking NSEventTrackingRunLoopMode (Cocoa) 此模式下用于处理窗口事件,鼠标事件等
      Common Modes NSRunLoopCommonModes (Cocoa) kCFRunLoopCommonModes (Core Foundation) 此模式用于配置”组模式”,一个输入源与此模式关联,则输入源与组中的所有模式相关联。
      我们也可以自定义模式,可以参考ASIHttpRequest在同步执行时,自定义了 runLoop 的模式叫 ASIHTTPRequestRunLoopMode。ASI的Timer源就关联了此模式。
      3.5 常见问题一:为什么TableView滑动时,Timer暂停了?
      我们做个测试: 在一个 viewController 的 scrollViewWillBeginDecelerating: 方法里面打个断点, 然后滑动 tableView。 待断点处, 使用 lldb 打印一下 [NSRunLoop currentRunLoop] 。 在描述中可以看到当前的RunLoop的运行模式:
      current mode = UITrackingRunLoopMode
      common modes =
0 0
原创粉丝点击