iOS学习
- NSOperation
-
-
- 使用
-
- [使用子类 `NSInvocationOperation`](#使用子类
NSInvocationOperation
) - [使用子类 `NSBlockOperation`](#使用子类
NSBlockOperation
) - [使用自定义继承自 NSOperation 的子类](#使用自定义继承自 NSOperation 的子类)
- [使用子类 `NSInvocationOperation`](#使用子类
-
- 使用NSOperationQueue
NSOperation
NSOperation、NSOperationQueue 是基于 GCD 更高一层的封装,完全面向对象。但是比 GCD 更简单易用、代码可读性也更高。
优点
- 可添加完成的代码块,再操作完成后执行
- 添加操作之前的依赖关系,方便控制执行顺序
- 设定操作执行的优先级
- 可以方便的取消一个操作的执行
- 使用KVO观察对操作执行状态的更改:isExecuteing、isFinished、isCancelled
NSOperation是基于GCD对封装,因此在NSOperation中也有类似的任务 (操作)和队列(操作队列)的概念
操作:
- NSOperation的操作就是在线程中执行的代码,相当于GCD的block块中要执行的任务
- NSOperation本身是个抽象类,没有定义具体的操作
操作队列
- 操作队列就是用来存放操作的队列,在这一点上跟GCD中存放任务的调度队列差不多。但是不同于 GCD 中的调度队列 FIFO(先进先出)的原则。NSOperationQueue 对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。
- 操作队列通过设置最大并发数,来控制并发,串行。
NSOperationQueue
提供了两种不同类型的队列:主队列 和自定义队列 。- 主队列运行在主线程之上
- 而自定义队列在后台执行
使用
NSOperation本身是一个抽象类,没有定义具体的操作逻辑,因此我们需要使用他的子类:
- 子类NSInvocationOperation
- 子类NSBlockOperation
- 自定义继承自Operation的子类,通过实现内部相应的方法来封装操作。
注意:如果单独使用NSOperation而不使用NSOperation Queue 的话,系统会执行同步操作
使用子类 NSInvocationOperation
objc
- (void)useInvocationOperation {
// 1.创建 NSInvocationOperation 对象
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
// 2.调用 start 方法开始执行操作
[op start];
}
/**
* 任务1
*/
- (void)task1 {
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}
objectivec
//在其他线程使用子类 NSInvocationOperation
[NSThread detachNewThreadSelector:@selector(useInvocationOperation) toTarget:self withObject:nil];
没有使用NSOperation Queue并且单独使用子类NSInvocationOperation执行一个操作的情况下,操作是在当前线程执行的,并没有开启新线程
使用子类 NSBlockOperation
objectivec
- (void)useBlockOperation {
// 1.创建 NSBlockOperation 对象
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
// 2.调用 start 方法开始执行操作
[op start];
//添加Block,并发执行,当所有Block都执行完,这个operation才视为完成。
[op addExecutionBlock:^{
NSLog(@"block2,线程:%@", [NSThread currentThread]);
}];
}
同样是在当前线程执行操作,不开启线程
NSBlockOperation还提供了一个方法 addExecutionBlock:
,通过方法 addExecutionBlock:
,就可以为NSBlockOperation添加额外的操作。这些操作可以在不同的线程中同时(并发)执行。只有当所有相关的操作已经完成执行时,才视为完成。
如果添加的操作多的话, blockOperationWithBlock:
中的操作也可能会在其他线程(非当前线程)中执行,这是由系统决定的,并不是说添加到 blockOperationWithBlock:
中的操作一定会在当前线程中执行。
objectivec
- (void)useBlockOperationAddExecutionBlock {
// 1.创建 NSBlockOperation 对象
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
// 2.添加额外的操作
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"5---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"6---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"7---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"8---%@", [NSThread currentThread]); // 打印当前线程
}
}];
// 3.调用 start 方法开始执行操作
[op start];
}

从上述结果可以看出:blockOperationWithBlock:
方法中的操作 和 addExecutionBlock:
中的操作是在不同的线程中异步执行的,并且所有方法都不固定在主线程执行。
所以 blockOperationWithBlock:
中的操作也可能会在其他线程(非当前线程)中执行。
使用自定义继承自 NSOperation 的子类
首先创建一个继承于NSOperation的子类,接着在该类的实现部分重写main
方法。
-
只要你不需要并发,只需要重写 main 方法即可,任务结束时自动完成。
-
如果你要实现并发操作(比如异步网络请求),就必须手动管理 isExecuting 和 isFinished,并重写 -start 方法。
objectivec
#import "CustomOperation.h"
@implementation CustomOperation
- (void)main {
if (!self.isCancelled) {
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2];
NSLog(@"1---%@", [NSThread currentThread]);
}
}
}
@end
- (void)useCustomOperation {
//1.创建CustomOperation对象
CustomOperation *op = [[CustomOperation alloc] init];
// 2.调用 start 方法开始执行操作
[op start];
}
使用NSOperationQueue
NSOperation Queue一共有两种队列:主队列、自定义队列。其中自定义队列同时包含了串行、并发功能。
创建
- 主队列:凡是添加到主队列中的操作,都会放到主线程中执行
objectivec
//获取主队列
NSOperationQueue *queue = [NSOperationQueue mainQueue];
- 自定义队列:添加到这种队列中的操作,会放到子线程中执行,同时包含了串行,并发功能
objectivec
//自定义队列创建方法
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
将操作添加到队列
这里有两个API:
- (void)addOperation:(NSOperation *)op;
:需要先创建操作,再将创建好的操作加入到创建好的队列中去。(void)addOperationWithBlock:(void (^)(void))block;
:无需先创建操作,在 block 中添加操作,直接将包含操作的 block 加入到队列中
objectivec
/**
* 使用 addOperation: 将操作加入到操作队列中
*/
- (void)addOperationToQueue {
// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.创建操作
// 使用 NSInvocationOperation 创建操作1
NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
// 使用 NSInvocationOperation 创建操作2
NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task2) object:nil];
// 使用 NSBlockOperation 创建操作3
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op3 addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
}
}];
// 3.使用 addOperation: 添加所有操作到队列中
[queue addOperation:op1]; // [op1 start]
[queue addOperation:op2]; // [op2 start]
[queue addOperation:op3]; // [op3 start]
//主线程会提前退出时,可以使用这个保证所有任务执行完再退出主程序。
[queue waitUntilAllOperationsAreFinished];
}

使用NSOperation子类创建操作,并使用 addOperation:
将操作加入到操作队列后能够开启新线程,进行并发执行
objectivec
/**
* 使用 addOperationWithBlock: 将操作加入到操作队列中
*/
- (void)addOperationWithBlockToQueue {
// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.使用 addOperationWithBlock: 添加操作到队列中
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
}
}];
}
控制串行、并发执行(最大并发操作数)
使用NSOperation Queue创建的自定义队列同时具有串行、并发执行的能力,这里的关键是maxConcurrentOperationCount
这个属性,叫做最大并发操作数,用来控制一个特定队列中可以用多少个操作同时参与并发执行。
maxConcurrentOperationCount
控制的不是并发线程的数量,而是一个队列中同时能并发执行的最大操作数。而且一个操作也并非只能在一个线程中运行。
maxConcurrentOperationCount
默认情况下为-1,表示不进行限制,可进行并发执行。maxConcurrentOperationCount
为1时,队列为串行队列。只能串行执行。maxConcurrentOperationCount
大于1时,队列为并发队列。
objectivec
//设置并发数
- (void)cjl_testOperationMaxCount{
/*
在GCD中只能使用信号量来设置并发数
而NSOperation轻易就能设置并发数
通过设置maxConcurrentOperationCount来控制单次出队列去执行的任务数
*/
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.name = @"Felix";
queue.maxConcurrentOperationCount = 2;
for (int i = 0; i < 5; i++) {
[queue addOperationWithBlock:^{ // 一个任务
[NSThread sleepForTimeInterval:2];
NSLog(@"%d-%@",i,[NSThread currentThread]);
}];
}
}
添加依赖
能够添加依赖时NSOperation的特点之一,通过依赖关系可以控制不同操作之间的执行顺序
NSOperation提供3个接口供我们管理和查看依赖:
- (void)addDependency:(NSOperation *)op;
添加依赖,使当前操作依赖于操作 op 的完成。- (void)removeDependency:(NSOperation *)op;
移除依赖,取消当前操作对操作 op 的依赖。@property (readonly, copy) NSArray<NSOperation *> *dependencies;
在当前操作开始执行之前完成执行的所有操作对象数组。也就是说当前操作要等操作对象数组中所有操作执行完后才能进行
实际使用场景如下:
objectivec
- (void)cjl_testOperationDependency{
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *bo1 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:0.5];
NSLog(@"请求token");
}];
NSBlockOperation *bo2 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:0.5];
NSLog(@"拿着token,请求数据1");
}];
NSBlockOperation *bo3 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:0.5];
NSLog(@"拿着数据1,请求数据2");
}];
[bo2 addDependency:bo1];
[bo3 addDependency:bo2];
[queue addOperations:@[bo1,bo2,bo3] waitUntilFinished:YES];
NSLog(@"执行完了?我要干其他事");
}
优先级
NSOperation 提供了queuePriority
(优先级)属性,queuePriority
属性适用于同一操作队列中的操作,不适用于不同操作队列中的操作。
默认情况下,所有新创建的操作对象优先级都是NSOperationQueuePriorityNormal
。但是我们可以通过setQueuePriority:
方法来改变当前操作中同一队列中的执行优先级
objectivec
// 优先级的取值,从上到下优先级越来越高
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};
NSOperation设置优先级只会让CPU有更高的几率调用,不是说设置高就一定全部先完成。
对于添加到队列中到操作,分为未开始状态,准备就绪状态,再到开始执行。
从未开始状态走到就绪状态,取决于操作之间的依赖关系:比如说op2依赖于op1,那么如果op1没有执行完,op2就无法走到就绪状态。
从就绪状态到执行状态,由操作之间的相对优先级决定:op1的优先级如果比op3高,那么会优先op1执行,但是由于各种原因,比如任务大小,有可能op3先完成。
- 依赖关系决定"能不能执行",优先级决定"更有可能先被调度"。
线程间通信
一般在主线程里边进行 UI 刷新,例如:点击、滚动、拖拽等事件。把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作。而当我们有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯。
objectivec
//线程间通讯
- (void)cjl_testOperationNoti{
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.name = @"Felix";
//异步实现耗时操作
[queue addOperationWithBlock:^{
NSLog(@"请求网络%@--%@", [NSOperationQueue currentQueue], [NSThread currentThread]);
//回到主线程
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"刷新UI%@--%@", [NSOperationQueue currentQueue], [NSThread currentThread]);
}];
}];
}
线程同步和安全
在多线程中如果一个线程在对临界区的资源进行读写操作,此时另一个线程也对同一片临界区的资源进行读写操作,如果第一个线程写完还没来得及读就被第二个线程进行了写的操作,那么第一个线程读取的资源不是本来的资源而是被篡改后的资源。此时这两个线程就是不安全的。
在前面的NSThread我们已经了解了这两个概念。使用NSOperation和NSOperation queue实现和NSThread逻辑基本一样,线程安全的也是通过加锁实现,不再演示。
常用属性和方法归纳
NSOperation
取消操作:
- (void)cancel;
可取消操作,实质是标记 isCancelled 状态。
判断操作状态方法
- (BOOL)isFinished;
判断操作是否已经结束。- (BOOL)isCancelled;
判断操作是否已经标记为取消。- (BOOL)isExecuting;
判断操作是否正在在运行。- (BOOL)isReady;
判断操作是否处于准备就绪状态,这个值和操作的依赖关系相关。
操作同步
- (void)waitUntilFinished;
阻塞当前线程,直到该操作结束。可用于线程执行顺序的同步。- (void)setCompletionBlock:(void (^)(void))block;
completionBlock
会在当前操作执行完毕时执行 completionBlock。- (void)addDependency:(NSOperation *)op;
添加依赖,使当前操作依赖于操作 op 的完成。- (void)removeDependency:(NSOperation *)op;
移除依赖,取消当前操作对操作 op 的依赖。@property (readonly, copy) NSArray<NSOperation *> *dependencies;
在当前操作开始执行之前完成执行的所有操作对象数组。
**NSOperationQueue **
取消/暂停/恢复操作
-
- (void)cancelAllOperations;
可以取消队列的所有操作。 -
- (BOOL)isSuspended;
判断队列是否处于暂停状态。 YES 为暂停状态,NO 为恢复状态。 -
- (void)setSuspended:(BOOL)b;
可设置操作的暂停和恢复,YES 代表暂停队列,NO 代表恢复队列。
操作同步
- (void)waitUntilAllOperationsAreFinished;
阻塞当前线程,直到队列中的操作全部执行完毕。
添加/获取操作
-
- (void)addOperationWithBlock:(void (^)(void))block;
向队列中添加一个 NSBlockOperation 类型操作对象。 -
- (void)addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait;
向队列中添加操作数组,wait 标志是否阻塞当前线程直到所有操作结束 -
- (NSArray *)operations;
当前在队列中的操作数组(某个操作执行结束后会自动从这个数组清除)。 -
- (NSUInteger)operationCount;
当前队列中的操作数。
获取队列
+ (id)currentQueue;
获取当前队列,如果当前线程不是在 NSOperationQueue 上运行则返回 nil。+ (id)mainQueue;
获取主队列。
这里的暂停和取消(包括操作的取消和队列的取消)并不代表可以将当前的操作立即取消,而是当当前的操作执行完毕之后不再执行新的操作。
暂停和取消的区别就在于:暂停操作之后还可以恢复操作,继续向下执行;而取消操作之后,所有的操作就清空了,无法再接着执行剩下的操作。
总结
1.NSOperation相比于iOS其它多线程技术的特点有:
-
可添加完成的代码块,在操作完成后执行。
-
添加操作之间的依赖关系,方便的控制执行顺序。
-
设定操作执行的优先级。
-
可以很方便的取消或暂停一个操作的执行。
-
使用 KVO 观察对操作执行状态的更改:isExecuteing、isFinished、isCancelled。
-
在自定义的队列中可以设置最大并发数
2.主队列操作在主线程上进行,自定义队列的操作的后台执行
3.NSOperation 单独使用时,系统同步执行操作,配合NSOperation才能更好的实现异步操作
4.单独使用NSBlockOperation如果通过 addExecutionBlock:
为 NSBlockOperation 添加额外的操作,这些操作可以在不同线程并发执行,并且如果添加操作足够多那么blockOperationWithBlock:
中的操作也可能会在其他线程(非当前线程)中执行
5.NSOperationQueue
一共有两种队列:主队列 、自定义队列。其中自定义队列同时包含了串行、并发功能。
6.maxConcurrentOperationCount
控制的不是并发线程的数量,而是一个队列中同时能并发执行的最大操作数。而且一个操作也并非只能在一个线程中运行。
7.通过依赖关系可以控制不同的操作之间的执行顺序,操作依赖对操作的作用能力大于优先级对操作的作用能力。
8.优先级只适用于同一操作队列中的操作,对于不同操作队列的操作没作用,而操作依赖不仅适用于同一操作队列的操作,还适用于不同操作队列的操作
9.添加到队列中的操作处于未开始 的状态,只有当操作对其他的依赖都完成时才进入准备就绪状态,在准备就绪状态开始由优先级来决定操作的执行顺序,这也说明了为什么操作依赖对操作的作用能力大于优先级对操作的作用能力
10.线程安全通常需要线程同步来实现,线程同步又需要依靠锁来实现。
11.如果只重写了NSOperation的main方法,底层会为我们控制变更任务执行完成状态,以及任务退出。
如果想实现并发执行,那么就需要重写NSOperation的start方法,需要我们自行控制任务状态,在合适的时机去修改对应的isFinished。
-
必须重写 isAsynchronous 返回 YES。
-
必须手动 KVO 通知 isExecuting 和 isFinished 的变化。
-
任务完成时调用 completeOperation。
-
在异步任务(如网络请求、GCD、定时器等)时,都要用这种写法。
objectivec
#import <Foundation/Foundation.h>
@interface AsyncOperation : NSOperation
@end
#import "AsyncOperation.h"
@interface AsyncOperation ()
@property (nonatomic, assign) BOOL executing;
@property (nonatomic, assign) BOOL finished;
@end
@implementation AsyncOperation
@synthesize executing = _executing;
@synthesize finished = _finished;
// 必须重写
- (BOOL)isAsynchronous {
return YES;
}
- (BOOL)isExecuting {
return _executing;
}
- (BOOL)isFinished {
return _finished;
}
- (void)start {
// 1. 检查是否被取消
if (self.isCancelled) {
[self willChangeValueForKey:@"isFinished"];
_finished = YES;
[self didChangeValueForKey:@"isFinished"];
return;
}
// 2. 标记为正在执行
[self willChangeValueForKey:@"isExecuting"];
_executing = YES;
[self didChangeValueForKey:@"isExecuting"];
// 3. 启动异步任务
[self main];
}
- (void)main {
// 这里模拟一个异步任务,比如网络请求
NSLog(@"异步任务开始,线程:%@", [NSThread currentThread]);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
NSLog(@"异步任务完成,线程:%@", [NSThread currentThread]);
[self completeOperation];
});
}
// 4. 任务完成时调用
- (void)completeOperation {
[self willChangeValueForKey:@"isExecuting"];
[self willChangeValueForKey:@"isFinished"];
_executing = NO;
_finished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}
@end
#import <Foundation/Foundation.h>
#import "AsyncOperation.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
AsyncOperation *op = [[AsyncOperation alloc] init];
[queue addOperation:op];
[queue waitUntilAllOperationsAreFinished];
NSLog(@"所有任务完成");
}
return 0;
}
任务执行状态控制
isReady 当前任务是否处于就绪状态
isExecuting 当前任务是否处于正在执行中状态
isFinished 当前任务是否已执行完毕
isCancelled 当前任务是否已取消
在NSOperation的start方法源码中,在start方法内
- 首先创造一个自动释放池,然后获取线程优先级
- 做一系列的状态异常判断,然后判断当前状态是否isExecuting
- 如果不是,那么我们手动变成isExecuting,然后判断当前任务是否有被取消
- 若未被取消就调用NSOperation的main方法
- 再之后,调用NSOperation的finish方法。finish 方法中:在内部通过KVO的方式去变更isExecuting状态为isFinished状态
- 之后调用自动释放池的release
12.系统是怎样移除一个isFinished = YES的NSoperation的?
通过KVO方式来移除NSOperation queue中的NSOperation的