「OC」多线程(三)------NSOperation
文章目录
前言
在写了知乎日报之前学习了相关的GCD和NSThread的相关内容,然后在知乎日报写完之后,我继续开始OC之中多线程的NSOperation的学习。由于之后肯定是要对AFNetworking
和SDWebImage
的源码继续相关学习,所以学习NSOperation的相关内容也是给后面做铺垫。
介绍
NSOperation 是苹果公司对 GCD 的封装,完全面向对象,并比GCD多了一些更简单实用的功能,所以使用起来更加方便易于理解。NSOperation 和NSOperationQueue 分别对应 GCD 的 任务 和 队列。
首先我们来看看,关于GPT给出的GCD和NSOperation的区别
特性 | NSOperation | GCD |
---|---|---|
抽象级别 | 面向对象,高抽象 | 基于 C 语言,低抽象 |
任务依赖 | 支持,任务之间可设置依赖关系 | 不支持,需要手动控制 |
取消任务 | 支持任务取消并可检测取消状态 | 不支持(任务一旦开始,无法取消) |
任务管理 | 可管理任务状态(开始、取消、完成等) | 无任务状态管理 |
队列控制 | 最大并发数、优先级、依赖性 | 控制较少,需手动实现 |
实现的具体步骤
-
将需要执行的操作封装到一个NSOperation对象中
-
将NSOperation对象添加到NSOperationQueue中系统会自动将NSOperationQueue中的NSOperation取出来,并将取出的NSOperation封装的操作放到一条新线程中执行
NSOperation的创建
NSOperation是个抽象类,并不具备封装操作的能力,必须使用它的子类------使用NSOperation子类的方式有3种
- NSInvocationOperation
objc
/*
第一个参数:目标对象
第二个参数:选择器,要调用的方法
第三个参数:方法要传递的参数
*/
NSInvocationOperation *op = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(download) object:nil];
//启动操作
[op start];
- NSBlockOperation(最常用)
objc
//1.封装操作
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
//要执行的操作,在主线程中执行
NSLog(@"1------%@",[NSThread currentThread]);
}];
//2.追加操作,追加的操作在子线程中执行,可以追加多条操作
[op addExecutionBlock:^{
NSLog(@"---download2--%@",[NSThread currentThread]);
}];
[op start];
- 自定义子类继承NSOperation,实现内部相应的方法
objc
// 重写自定义类的main方法实现封装操作
-(void)main
{
// 要执行的操作
}
// 实例化一个自定义对象,并执行操作
JCOperation *op = [[JCOperation alloc]init];
[op start];
自定义类封装性高,复用性高。
NSOperationQueue的使用
NSOperation中的两种队列
- 主队列:通过mainQueue获得,凡是放到主队列中的任务都将在主线程执行
- 非主队列:直接alloc init出来的队列。非主队列同时具备了并发和串行的功能,通过设置最大并发数属性来控制任务是并发执行还是串行执行
objc
// 主队列获取方法
NSOperationQueue *queue = [NSOperationQueue mainQueue];
objc
// 自定义队列创建方法
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSOperationQueue的作用
- NSOperation可以调用start方法来执行任务,但默认是同步执行的
- 如果将NSOperation添加到NSOperationQueue(操作队列)中,系统会自动异步执行NSOperation中的操作
添加操作到NSOperationQueue中
- (void)addOperation:(NSOperation *)op;
- (void)addOperationWithBlock:(void (^)(void))block;
注意:将操作添加到NSOperationQueue中,就会自动启动,不需要再自己启动了,addOperation 内部调用 start方法 ,而start方法内部调用 main方法
使用实例
了解了以上相关的原理,接下来学习几个例子进行巩固
NSInvocationOperation的使用
objc
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(taskMethod) object:nil];
[operation start]; // 直接启动任务
- (void)taskMethod {
NSLog(@"任务执行中 - %@", [NSThread currentThread]);
}
这样的操作,默认任务是要在当前主线程之中完成的,如果我们想要实现异步操作的话,就需要和NSOperationQueue配合进行使用,不过这样一来代码就会较为的冗余,因此在实际运用之中我们不太使用。
NSBlockOperation的使用
objc
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务1 - %@", [NSThread currentThread]);
}];
// 添加额外任务
[operation addExecutionBlock:^{
NSLog(@"任务2 - %@", [NSThread currentThread]);
}];
[operation start];
任务的主块和附加块可能并发执行。而且,主任务是在主线程之中执行,而额外的任务则是在子线程之中执行
NSOperationQueue的使用
objc
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 创建任务
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务1 - %@", [NSThread currentThread]);
}];
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务2 - %@", [NSThread currentThread]);
}];
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务3 - %@", [NSThread currentThread]);
}];
// 将任务加入队列
[queue addOperation:operation1];
[queue addOperation:operation2];
[queue addOperation:operation3];
-
队列会自动创建多个线程并发执行任务。
-
默认并发执行任务,可以通过设置
maxConcurrentOperationCount
调整并发数。
取消操作
我们在任务进行的时候通过调用cancel
的方法对任务进行取消,不过值得注意的是,当我们取消任务的时候,任务并不会自动停止,而是需要我们通过调用任务的isCancelled
的方法进行判断,我们用一个简单的例子实现方式如下
objc
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
__block NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 5; i++) {
if ([operation isCancelled]) {
NSLog(@"任务被取消");
return;
}
NSLog(@"任务执行中:%d", i);
[NSThread sleepForTimeInterval:1]; // 模拟耗时任务
}
NSLog(@"任务完成");
}];
[queue addOperation:operation];
// 2秒后取消任务
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[operation cancel];
});
我们可以将if ([operation isCancelled])
的内容注释,这时候我们可以发现,任务会直接执行完整,而不是到任务2的时候就中断了。
最大并发数
前面我们说到了,NSOperationQueue之中每添加一个任务,都会将任务添加到一个子线程之中,那如果不想要让这个队列开这么多个并发的线程,我们就可以设置这个队列之中的属性maxConcurrentOperationCount
objc
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 2; // 最多并发2个任务
for (int i = 0; i < 5; i++) {
[queue addOperationWithBlock:^{
NSLog(@"任务%d - %@", i, [NSThread currentThread]);
[NSThread sleepForTimeInterval:1]; // 模拟耗时任务
}];
}
自定义NSOperation子类
当内置的 NSBlockOperation
和 NSInvocationOperation
无法满足需求时,可以继承 NSOperation
自定义复杂任务。
相关属性和方法
objc
// 对于并发的Operation需要重写改方法
- (void)start;
// 非并发的Operation需要重写该方法
- (void)main;
// 任务是否取消(只读) 自定义子类,需重写该属性
@property (readonly, getter=isCancelled) BOOL cancelled;
// 可取消操作,实质是标记 isCancelled 状态,自定义子类,需利用该方法标记取消状态
- (void)cancel;
// 任务是否正在执行(只读),自定义子类,需重写该属性
@property (readonly, getter=isExecuting) BOOL executing;
// 任务是否结束(只读),自定义子类,需重写该属性
// 如果为YES,则队列会将任务移除队列
@property (readonly, getter=isFinished) BOOL finished;
// 判断任务是否为并发(只读),默认返回NO
// 自定义子类,需重写getter方法,并返回YES
@property (readonly, getter=isAsynchronous) BOOL asynchronous;
// 任务是否准备就绪(只读)
// 对于加入队列的任务来说,ready为YES,则表示该任务即将开始执行
// 如果存在依赖关系的任务没有执行完,则ready为NO
@property (readonly, getter=isReady) BOOL ready;
对于NSOperation
.如果要自定义的话,我们需要重写start或者main,这两个方法的其中之一,自定义的是并发的NSOperation
.那么我们得重写start
方法.如果是非并发的话就只需要将相关逻辑写在main
方法之中。接下来我们介绍一下相关的main和start方法的区别
start方法
start
方法是 NSOperation
的入口点,表示任务开始执行。
当调用 start
方法时,NSOperation
会根据其内部状态决定如何执行任务:
- 如果操作已被取消(即
isCancelled == YES
),它会直接标记为完成,而不会执行。 - 如果未被取消:
- 在主线程中调用
main
方法(默认情况下)。 - 如果操作是异步的(需要自定义实现),
start
方法通常会启动一个异步任务
- 在主线程中调用
默认的start
方法逻辑大致如下
objc
- (void)start {
if (self.isCancelled) {
[self finish];
return;
}
[self setIsExecuting:YES];
[self main];
[self setIsExecuting:NO];
[self finish];
}
main方法
main
是任务的实际执行内容。
默认情况下,NSOperation
的 main
方法是空的。开发者需要通过子类化 NSOperation
并重写 main
方法来定义任务的具体逻辑。
main
方法在调用 start
时被执行。一般来说main就是用来完成任务的具体逻辑,但一般来说,如果只是为了串行的完成相关任务,其实代码不需要放置在main
方法之中,相关处理放在可见的控制器之中即可
重写NSOperation实现并发
了解了相关的方法和属性之后,其实如何让NSOperation实现并发其实也不算太难
objc
@interface JCOperation : NSOperation
@end
@implementation JCOperation {
BOOL _isExecuting;
BOOL _isFinished;
}
- (instancetype)init {
self = [super init];
if (self) {
_isExecuting = NO;
_isFinished = NO;
}
return self;
}
- (void)start {
if (self.isCancelled) {
[self finish];
return;
}
[self setIsExecuting:YES];
// 异步操作开始
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self main];
});
}
- (void)main {
// 模拟异步任务(网络请求等)
[NSThread sleepForTimeInterval:2];
NSLog(@"任务完成 - %@", [NSThread currentThread]);
[self finish];
}
- (void)finish {
[self setIsExecuting:NO];
[self setIsFinished:YES];
}
// 重写状态方法,getter方法
- (BOOL)isExecuting { return _isExecuting; }
- (BOOL)isFinished { return _isFinished; }
- (void)setIsExecuting:(BOOL)isExecuting {
//手动触发KVO
[self willChangeValueForKey:@"isExecuting"];
_isExecuting = isExecuting;
[self didChangeValueForKey:@"isExecuting"];
}
- (void)setIsFinished:(BOOL)isFinished {
[self willChangeValueForKey:@"isFinished"];
_isFinished = isFinished;
[self didChangeValueForKey:@"isFinished"];
}
@end
在重写NSOperation子类当中,我们需要使用KVO来实现对应属性的改变,由于KVO 机制只能监听 属性 ,也就是类提供的 @property
定义的公开或受保护的接口。而在NSOperation
之中其 isExecuting
和 isFinished
属性是只读的,我们无法直接使用 self.executing = YES;
来修改其值。
- 当我们自定义
NSOperation
子类时,_isExecuting
和_isFinished
这两个实例变量代替了原有的只读属性isExecuting
和isFinished
。 - 在内部逻辑中,我们直接操作这些实例变量来控制操作的状态。
- 实际使用中,
NSOperationQueue
或其他部分不直接访问这些变量 ,而是通过isExecuting
和isFinished
的 getter 方法获取状态。(这个很关键)
KVO的关键就在于通知其他部分属性的变化 ,通过 KVO,我们在修改实例变量时,将变化广播给 NSOperationQueue
等外部监听方:
NSOperationQueue
使用 KVO 监听isExecuting
和isFinished
的变化。- 当
isFinished
变为YES
时,队列会认为操作已完成并从队列中移除,同时触发依赖任务的执行。
由于KVO 的工作原理要求使用完整的 getter 方法名称作为键路径,而不是属性名本身。所以我们使用KVO手动监听isExecuting
和 isFinished
objc
@property (readonly, getter=isExecuting) BOOL executing;
@property (readonly, getter=isFinished) BOOL finished;
简单的概括来说是:
实例变量是内部逻辑的真实值。
KVO 是广播机制 ,通知外部(尤其是 NSOperationQueue
)状态已经改变。