iOS多线程之 NSOperation 的基本使用

上篇文章学了 GCD 的基本使用,本文就来学习一下 iOS 的另一套多线程解决方案:NSOperation,它是基于 GCD 开发的,但是比 GCD 更加面向对象,并且拥有更强的可控性和代码可读性,NSOperation 是一个抽象基类,我们实际使用的是系统封装好的两个子类 NSInvocationOperationNSBlockOperation,NSOperation 需要配合NSOperationQueue 来实现多线程,默认情况下,NSOperation 单独使用时系统同步执行操作,不会开启新新线程,只有配合NSOperationQueue 才能实现任务的异步执行,NSOperation 和NSOperationQueue 实现多线程的具体步骤:

  • 创建任务:先将需要执行的任务封装到一个 NSOperation 对象中
  • 创建队列:创建一个 NSOperationQueue 对象
  • 任务加入队列:然后将 NSOperation 对象添加到 NSOperationQueue 对象中

之后,系统会⾃动将 NSOperationQueue 中的 NSOperation 任务取出,放入新线程中执⾏,下面看看详细的 NSOperation 和 NSOperationQueue 的详细使用

NSOperation 和 NSOperationQueue

Swfit 中 NSOperationNSOperationQueue 都去掉了前缀 NS,直接叫 OperationOperationQueue,下文仍然沿用 OC 的叫法,明白意思就行

NSInvocationOperation

苹果认为 NSInvocationOperation 不是类型安全或者不是 ARC 安全的,在 Swift中 取消了与之相关的 API,如果你使用 Swift 那么就只能使用 NSBlockOperation,所以这里只演示 OC 如何使用 NSInvocationOperation 创建操作对象

1
2
3
4
//创建操作对象
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download) object:nil];
//开始执行
[operation start];

NSBlockOperation

使用 NSBlockOperation 创建一个操作对象

1
2
3
4
5
let operation = BlockOperation {
print("任务---\(Thread.current)")
}
operation.start()

打印结果:

1
任务---<NSThread: 0x600000065280>{number = 1, name = main}

可以看到,在没有使用 NSOperationQueue,单独使用 NSBlockOperation 的情况下,NSBlockOperation 是在主线程执行操作,并没有开启新线程

来看一下另外一种情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let operation = BlockOperation {
print("任务1---\(Thread.current)")
}
operation.addExecutionBlock {
print("任务2---\(Thread.current)")
}
operation.addExecutionBlock {
print("任务3---\(Thread.current)")
}
operation.start()

打印结果:

1
2
3
任务1---<NSThread: 0x600000264e40>{number = 1, name = main}
任务3---<NSThread: 0x60800026a380>{number = 3, name = (null)}
任务2---<NSThread: 0x600000271e00>{number = 4, name = (null)}

NSBlockOperation 还提供了一个方法 addExecutionBlock,通过 addExecutionBlock 添加的任务会在其他线程中并发执行

NSOperationQueue

NSOperationQueue 不同于 GCD 的队列,它只有两种队列:主队列和其他队列,主队列对应的就是主线程队列,其他队列用来实现串行和并发的功能,只要操作对象添加到队列,就会自动自动调用操作对象的 start() 方法

创建队列

1
2
3
4
//创建主队列
let mainQueue = OperationQueue.main
//创建其他队列
let otherQueue = OperationQueue()

把操作任务添加到队列有两种方法:第一种,先创建任务操作对象,然后使用 addOperation() 把任务操作对象添加到队列,第二种,我们也可以不创建任务操作对象,直接使用队列添加任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let queue = OperationQueue()
let op1 = BlockOperation {
print("任务1---\(Thread.current)")
}
let op2 = BlockOperation {
print("任务2---\(Thread.current)")
}
//方法一:把操作对象添加到队列
queue.addOperation(op1)
queue.addOperation(op2)
//方法二:直接在队列中添加操作任务
queue.addOperation {
print("任务3---\(Thread.current)")
}

打印结果:

1
2
3
任务3---<NSThread: 0x60000007c800>{number = 4, name = (null)}
任务2---<NSThread: 0x60000026ac80>{number = 5, name = (null)}
任务1---<NSThread: 0x6080002626c0>{number = 3, name = (null)}

可以发现不管把任务操作对象添加到队列还是直接把任务添加到队列,都是并发的执行任务,当然如果是主队列的话,任务还是串行的在主线程执行的,那如果需要开启新线程异步串行呢?NSOperationQueue 有个属性 maxConcurrentOperationCount最大并发数

  • maxConcurrentOperationCount 默认情况下为 -1,表示不进行限制,即默认为并发执行
  • maxConcurrentOperationCount 为1时,进行串行执行。
  • 当maxConcurrentOperationCount 大于1时,进行并发执行,当然这个值不应超过系统限制,即使自己设置一个很大的值,系统也会自动调整

注意:maxConcurrentOperationCount 设置的是队列里面最多能并发运行的操作任务个数,而不是线程个数,当一个线程执行完毕后会有一个回收到线程池的过程,这时如果线程池中还有别的线程就会直接拿出来进行任务的执行,如果线程池中没有线程,就会等待回收后的线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
// queue.maxConcurrentOperationCount = 2
let op1 = BlockOperation {
print("任务1---\(Thread.current)")
}
let op2 = BlockOperation {
print("任务2---\(Thread.current)")
}
queue.addOperation(op1)
queue.addOperation(op2)
queue.addOperation {
print("任务3---\(Thread.current)")
}
queue.addOperation {
print("任务4---\(Thread.current)")
}

最大并发数为1,打印结果:

1
2
3
4
任务1---<NSThread: 0x17027d800>{number = 4, name = (null)}
任务2---<NSThread: 0x170461380>{number = 5, name = (null)}
任务3---<NSThread: 0x170273c80>{number = 3, name = (null)}
任务4---<NSThread: 0x170273c80>{number = 3, name = (null)}

最大并发数为2,打印结果:

1
2
3
4
任务2---<NSThread: 0x17407cc40>{number = 5, name = (null)}
任务1---<NSThread: 0x17407ba80>{number = 4, name = (null)}
任务3---<NSThread: 0x17026d600>{number = 6, name = (null)}
任务4---<NSThread: 0x17026d600>{number = 6, name = (null)}

可以看到当最大并发数为1时,任务是按顺序串行执行的,当最大并发数为2时,任务是并发执行的,顺序随机,但是开启线程的数量是由系统决定的,设为1就表示队列每次只能执行一个操作,因此串行化的NSOperationQueue 并不等同于GCD中的串行队列

NSOperation 和 NSOperationQueue 的其他用法

NSOperation 操作依赖

当某个 NSOperation 对象依赖于其它 NSOperation 对象的完成时,就可以通过 addDependency() 方法添加一个或者多个依赖的对象,只有所有依赖的对象都已经完成操作,当前 NSOperation 对象才会开始执行操作,另外,可以通过 removeDependency() 方法来删除依赖对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let queue = OperationQueue()
let op1 = BlockOperation {
print("任务1---\(Thread.current)")
}
let op2 = BlockOperation {
print("任务2---\(Thread.current)")
}
let op3 = BlockOperation {
print("任务3---\(Thread.current)")
}
//op3依赖于op1,则先完成op1,再完成op3
op3.addDependency(op1)
//op1依赖于op2,则先完成op2,再完成op1
op1.addDependency(op2)
//最终的依赖关系就是,op2->op1->op3
queue.addOperation(op1)
queue.addOperation(op2)
queue.addOperation(op3)

打印结果:

1
2
3
任务2---<NSThread: 0x608000072000>{number = 3, name = (null)}
任务1---<NSThread: 0x60000007f880>{number = 4, name = (null)}
任务3---<NSThread: 0x608000072000>{number = 3, name = (null)}

不管运行多少次,最终的打印结果都是:任务2 -> 任务1 -> 任务3

注意:不能添加相互依赖,会死锁,比如 A依赖B,B依赖A,可以在不同的队列之间依赖,依赖是添加到任务身上的,和队列没关系

取消 NSOperation

NSOperation 对象一旦添加到 NSOperationQueue 队列,这个队列就拥有了这个 NSOperation 对象,并且不能被删除,唯一能做的事情是取消,可以调用 NSOperation 对象的 cancel() 方法取消单个任务操作,也可以调用 NSOperationQueue 队列的cancelAllOperations()方法取消当前队列中的所有操作

1
2
3
4
//取消单个操作
operation.cancel()
//取消队列中所有的操作
queue.cancelAllOperations()

等待 NSOperation 完成

如果需要在当前线程中处理操作任务完成后的结果,可以使用NSOperation 的waitUntilFinished()方法阻塞当前线程,等待操作任务的完成,最好不要在主线程调用这个方法,这可能导致阻塞主线程影响用户体验

1
operation.waitUntilFinished()

除了等待单个操作对象任务完成,也可以同时等待一个队列的所有操作完成,使用 NSOperationQueue 的 waitUntilAllOperationsAreFinished() 方法,在等待一个队列时,应用的其他线程仍然可以往队列里添加操作任务对象,这样可能会加长等待时间

1
operation.waitUntilAllOperationsAreFinished()

暂停和继续 NSOperationQueue

如果你想临时暂停操作任务的执行,可以设置队列的 isSuspendedtrue,不过暂停一个队列不会影响当前的正在执行的操作任务,不过会停止新的操作对象执行,可以在响应用户请求的时候,暂停一个等待中的任务,完成用户的请求之后,再次调用 isSuspended 继续队列中的其他操作任务的执行

1
2
3
4
//暂停队列
queue.isSuspended = true
//继续队列
queue.isSuspended = false

NSOperation 到这里差不多就讲完了,本文只是提供了NSOperation 的各种方法的使用,实际开发中具体怎么把他们用到合适的地方,就需要多多实践了,有机会我写写多线程的实践案例

其他相关文章:

iOS 多线程之 NSThread 的基本使用

iOS多线程之GCD的基本使用

本人刚开始写博客,主要是为了给自己的知识点做一个笔记,方便自己以后查阅,如果能让别人有所启发也是荣幸之至!如有错误,欢迎指正!