NSOperation 和 NSOperationQueue

之前做 iOS 的并发编程的时候一直用的 GCD,NSOperation 用得很少,这个类对我来说一直比较神秘,于是最近仔细研究了一下。注意这篇文章并不会很详细讲到各个方面,只是解答一些刚开始接触 NSOperation 时会遇到的疑惑。

我们在使用 GCD 的时候,通常会将 block dispatch 到其他线程异步执行,就像这样:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
    // some task you want to perform asynchronously on other thread
}

这里的 block 就是我们希望异步执行的代码,这段代码被当做参数传入 dispatch_async 函数,然后它为我们开启一个新的线程,用 dispatch queue 来管理 block 的执行顺序。

其实 NSOperation 就是对我们需要异步执行的代码的面向对象的封装,上面的代码换作 NSOperation 会这么写:

let queue = NSOperationQueue()
queue.addOperationWithBlock { 
    // some task you want to perform asynchronously on other thread
}

你会发现上面的代码根本没有出现 NSOperation,而是出现了 NSOperationQueue,这是为什么呢?

因为前面已经说过,NSOperation 只是对执行代码的封装,本身并不提供任何的异步功能,NSOperationQueue 底层是基于 GCD 的封装,所以一般来说,我们会用 NSOperationQueue 来控制 operation 的执行,queue 会根据 operation 的优先级、依赖等来决定如何执行添加进 queue 的 operation。

之前的代码我们用了 NSOperationQueue 的方法 addOpertionWithBlock 来给 queue 方便的添加 operation,接下来我们来使用 NSOperation,但是 NSOperation 是一个“抽象”类(注意这里的抽象是指它功能上的抽象,OC 和 Swift 中并没有真正的抽象类的概念),所以你只能通过它的子类来使用它,系统已经提供了两个,NSBlockOperation 和 NSInvocationOperation(Swift 不支持后者),使用起来就像下面这样:

let queue = NSOperationQueue()
let operation = NSBlockOperation {
    // ...
}
queue.addOperation(operation)

使用 NSOperation 的好处是,你可以指定 operation 的优先级和依赖,当多个 operaton 添加到 queue 时,queue 可以根据这些来确定执行顺序。

当然,你也可以自定义 NSOperation 的子类,你可以选择定义同步的子类或者异步的子类。系统提供的两个子类都是同步的,你可以通过打印他们的 asynchronous 属性来查看。

为什么还需要定义异步的 NSOperation 子类呢?

之前没有提到的是,NSOperation 不仅可以通过添加到 queue 里面执行,还可以通过 start 方法执行,就像这样:

let operation = NSBlockOperation {
    // ...
}
operation.start()

对于上面这段代码,这样写基本是没有意义的,因为这段代码还是会在主线程执行,跟直接执行 block 里面的代码没有区别。

通过官方文档可以知道,如果你只想要一个同步的 NSOperation 的子类,你只需要重写 main 方法就行了,但是,要想让它异步,你需要干的事情更多。

如何实现我们待会再说,先再多说一点关于 NSOperation 和 NSOperationQueue 的关系。

之前提到 queue 能控制 operation 的执行顺序,它还能控制它开启的线程最多能执行的 operation 的个数。那么 queue 是如何知道 operation 是什么时候开始执行,什么执行完成,什么时候被取消的呢?

这与 operation 维护的状态机有关,operation 有四个布尔属性,它们的状态可以用下面这张图表示:

state.png

当一个 operation 的所有依赖都完成执行后,它就会进入 ready 状态,准备开始执行了,但是如果此时它已经被 cancel 了,那么它不会开始执行而是直接进入 finished 状态,反之那么它将进入 executing 状态,开始执行,执行结束后进入 finished 状态。

这里多说一下 NSOperation 的 cancel 方法,这个方法只会将它的 isCanceled 属性变为 true,其他的什么也不会做。也就是说,如果这个 operation 已经开始执行了的话,那么只有等到 operation 的代码执行结束后它的 state 才会变为 finished。

queue 就是根据这些状态量以及优先级和其他因素来确定 operation 的执行顺序的。operation 用到了 iOS 中一个比较重要的黑科技 KVO 来通知 queue 自身状态的改变,可以猜想的是,在 NSOperation 的默认实现中,比如 start 方法中,肯定含有不少的 KVO 代码。

注意能被 KVO 观察的属性一定是 KVC 的,因为这里的状态属性都是布尔类型,按照苹果的风格,这些属性的 getter 方法的前面都有 is 前缀,所以,这些属性的 keypath 都是 is 开头的。

回到如何实现异步的 NSOperation 上来,官方的文档说了三点:重写 start 方法,在合适的地方更新 executing 和 finished 的值并且产生 KVO 通知,重写 asynchronous 返回 true。

这里说一下 start 方法和 main 方法,start 方法是 operation 开始的入口,main 方法一般放具体需要执行的代码,在 NSOperation 的默认实现里,start 方法会调用 main 方法。

因为重写了 start 方法,所以 KVO 通知必须由你自己来管理,官方文档里是通过两个 ivar 来实现状态值的更新。

@interface MyOperation : NSOperation {
    BOOL executing;
    BOOL finished;
}

既然要实现异步的 NSOperation,那么就必然要把需要执行的代码放到其他线程去执行,官方文档里直接把 main 方法 detach 到一个新的线程去执行。

值得注意的是,main 方法并没有特别的作用,你完全可以写一个其他的方法来写你需要移步执行的代码。比如下面提到的 AFN 2.0 里面就没有用到 main 方法,而是自己写了operationDidStart 来做工作。

[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];

除了官方文档的这种实现方法外,业内用的最多的是用一个枚举来标示状态,这里用 Swift 来演示:

public class ConcurrentOperation: NSOperation {
  public enum State: String {
    case Ready, Executing, Finished
    
    private var keyPath: String {
      return "is" + rawValue
    }
  }
  
  public var state = State.Ready {
    willSet {
      willChangeValueForKey(newValue.keyPath)
      willChangeValueForKey(state.keyPath)
    }
    didSet {
      didChangeValueForKey(oldValue.keyPath)
      didChangeValueForKey(state.keyPath)
    }
  }
}

可以看到这样带来的效果很显著,在任何地方更新 state 属性都会发出相应的 KVO 通知。

OC 的版本可以去看 AFNetworking 2.x 的 AFURLConnectionOperation 的实现。另外,苹果在 wwdc 2015 session 226 的这个 demo 有关于异步 NSOperation 更完善复杂的封装。

讲到这里,相信你对 NSOperation 里面的一些疑惑点有了一些基本的认识,接下来更深入的使用,可以去看看官方文档或其他文章。

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

推荐阅读更多精彩内容