「OC」多线程(三)——NSOperation

「OC」多线程(三)------NSOperation

文章目录

前言

在写了知乎日报之前学习了相关的GCD和NSThread的相关内容,然后在知乎日报写完之后,我继续开始OC之中多线程的NSOperation的学习。由于之后肯定是要对AFNetworkingSDWebImage的源码继续相关学习,所以学习NSOperation的相关内容也是给后面做铺垫。

介绍

NSOperation 是苹果公司对 GCD 的封装,完全面向对象,并比GCD多了一些更简单实用的功能,所以使用起来更加方便易于理解。NSOperation 和NSOperationQueue 分别对应 GCD 的 任务 和 队列。

首先我们来看看,关于GPT给出的GCD和NSOperation的区别

特性 NSOperation GCD
抽象级别 面向对象,高抽象 基于 C 语言,低抽象
任务依赖 支持,任务之间可设置依赖关系 不支持,需要手动控制
取消任务 支持任务取消并可检测取消状态 不支持(任务一旦开始,无法取消)
任务管理 可管理任务状态(开始、取消、完成等) 无任务状态管理
队列控制 最大并发数、优先级、依赖性 控制较少,需手动实现

实现的具体步骤

  1. 将需要执行的操作封装到一个NSOperation对象中

  2. 将NSOperation对象添加到NSOperationQueue中系统会自动将NSOperationQueue中的NSOperation取出来,并将取出的NSOperation封装的操作放到一条新线程中执行

NSOperation的创建

NSOperation是个抽象类,并不具备封装操作的能力,必须使用它的子类------使用NSOperation子类的方式有3种

  1. NSInvocationOperation
objc 复制代码
    /*
     第一个参数:目标对象
     第二个参数:选择器,要调用的方法
     第三个参数:方法要传递的参数
     */
NSInvocationOperation *op  = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(download) object:nil];
//启动操作
[op start];
  1. NSBlockOperation(最常用)
objc 复制代码
//1.封装操作
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
       //要执行的操作,在主线程中执行
       NSLog(@"1------%@",[NSThread currentThread]); 
}];
//2.追加操作,追加的操作在子线程中执行,可以追加多条操作
[op addExecutionBlock:^{
        NSLog(@"---download2--%@",[NSThread currentThread]);
    }];
[op start];
  1. 自定义子类继承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子类

当内置的 NSBlockOperationNSInvocationOperation 无法满足需求时,可以继承 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 是任务的实际执行内容。

默认情况下,NSOperationmain 方法是空的。开发者需要通过子类化 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之中其 isExecutingisFinished 属性是只读的,我们无法直接使用 self.executing = YES; 来修改其值。

  • 当我们自定义 NSOperation 子类时,_isExecuting_isFinished 这两个实例变量代替了原有的只读属性 isExecutingisFinished
  • 在内部逻辑中,我们直接操作这些实例变量来控制操作的状态。
  • 实际使用中,NSOperationQueue 或其他部分不直接访问这些变量 ,而是通过 isExecutingisFinishedgetter 方法获取状态。(这个很关键)

KVO的关键就在于通知其他部分属性的变化 ,通过 KVO,我们在修改实例变量时,将变化广播给 NSOperationQueue 等外部监听方:

  1. NSOperationQueue 使用 KVO 监听 isExecutingisFinished 的变化。
  2. isFinished 变为 YES 时,队列会认为操作已完成并从队列中移除,同时触发依赖任务的执行。

由于KVO 的工作原理要求使用完整的 getter 方法名称作为键路径,而不是属性名本身。所以我们使用KVO手动监听isExecutingisFinished

objc 复制代码
@property (readonly, getter=isExecuting) BOOL executing;
@property (readonly, getter=isFinished) BOOL finished;

简单的概括来说是:

实例变量是内部逻辑的真实值

KVO 是广播机制 ,通知外部(尤其是 NSOperationQueue)状态已经改变。

参考文章

NSOperation

彻底了解NSOperation的自定义

iOS Operation 自定义的注意点

相关推荐
cwtlw4 分钟前
CSS学习记录11
前端·css·笔记·学习·其他
是十一月末6 分钟前
Linux的基本功能和命令
linux·服务器·开发语言·数据库
Dingdangr16 分钟前
iOS中的类型推断及其在Swift编程语言中的作用和优势
开发语言·ios·swift
legendary_16320 分钟前
LDR6500:音频双C支持,数字与模拟的完美结合
c语言·开发语言·网络·计算机外设·电脑·音视频
愿尽32 分钟前
C#--方法
开发语言·c#
cwtlw36 分钟前
如何创建maven工程
java·笔记·后端·学习·maven
froginwe111 小时前
XML 查看器:深入理解与高效使用
开发语言
F20226974861 小时前
使用 Python 爬取某网站简历模板(bs4/lxml+协程)
开发语言·python
一行玩python1 小时前
Xerces-C,一个成熟的 C++ XML 解析库!
xml·c语言·开发语言·c++
我命由我123451 小时前
15.Java 网络编程(网络相关概念、InetAddress、NetworkInterface、TCP 网络通信、UDP 网络通信、超时中断)
java·开发语言·网络·后端·tcp/ip·udp·java-ee