【iOS】NSOperation与NSOperationQueue

【iOS】NSOperation与NSOperationQueue

前言

在iOS开发中,实现多线程常见方案包括NSThread、GCD、NSOperation和NSOperationQueue。

NSOperation和NSOperationQueue是基于GCD更高一层的封装,完全面向对象。与GCD相比,更适合处理具有复杂调度关系的任务。

  1. NSOperation

是用于封装一个需要执行的任务,可以理解为对GCD中任务代码的面向对象封装。NSOperation本身是一个抽象基类,通常不能直接使用。需要使用其子类进行操作:

  • NSInvocationOperation
  • NSBlockOperation
  • 继承自NSOperation的子类,通过实现内部相应方法来封装操作

与GCD相比,GCD轻量、灵活、执行效率高,适合简单异步任务和底层调度;NSOperation结构化和可管理性更强,适合存在多个任务、依赖关系或取消需求的复杂场景。

优点在于:

  • 可添加完成的代码块,在操作完后执行
  • 添加操作之前的依赖关系,方便控制执行顺序
  • 设定操作执行的优先级
  • 可通过cancel方法
  1. NSOperationQueue

NSOperationQueue用于管理和调度多个NSOperation。

GCD调度队列遵循FIFO先进先出的原则,而NSOperationQueue操作队列是根据自身依赖、取消状态以及队列调度条件等决定是否可以执行。依赖完成、操作未被取消且队列有可用并发资源的操作会被调度执行。操作的实际调度主要依赖以下因素:

  • 操作之间的依赖关系
  • 操作优先级(优先级是操作对象自身的属性)
  • 队列允许的最大并发操作数
  • Qos等级
  • 系统资源状况

Qos:服务质量,用于表示任务的重要程度,系统会根据此决定任务获得多少调度优先级、CPU和响应速度。

NSOperationQueue提供了两种不同类型的队列:

  • 主队列:运行在主线程上
  • 自定义队列:运行在后台

使用NSOperation

单独使用NSOperation不使用NSOperationQueue,系统会执行同步操作。NSOperation只是任务对象,本身不会主动调度队列

  1. 使用子类NSInvocationOperation
objc 复制代码
@implementation DemoClass

- (void)useInvocationOperation {
    NSLog(@"132131");
    NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task) object:nil];
    [op start];
}

- (void)task {
    for (int i = 0; i < 2; i++) {
        [NSThread sleepForTimeInterval:2.0];
        NSLog(@"1---%@", [NSThread currentThread]);
    }
}

@end

从打印日志可以看出,单独使用子类NSInvocatrionOperation不使用NSOperationQueue的操作是在当前线程执行的,并没有开启新线程。这里主要是因为start方法不会自动创建新线程,而是在当先线程中同步执行

  1. 使用子类NSBlockOperation

从日志我们看出,同样是在当前线程执行操作,不开启新线程。

另外NSBlockOperation还提供了一个addExecutionBlock:方法。通过该方法,可以为NSBlockOperation添加额外的操作,这些操作可以在不同线程中并发执行。只有当所有操作执行完后,才视为完成,可以调用start方法。start方法会同步执行整个操作,调用后不能再追加Block。

然而,如果添加到blockOperationWithBlock:的操作多的话,其中操作也可能会在其他线程中执行,这取决于系统。因此添加到blockOperationWithBlock:的操作不一定在当前线程中执行

objc 复制代码
- (void)useBlockOperationAddExecutionBlock {
    // 创建NSBlockOperation对象
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"1---%@", [NSThread currentThread]);
        }
    }];

    // 添加额外操作
    [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]);
        }
    }];

    [op start];
}

从打印日志我们可以看出blockOperationWithBlock:方法的操作和addExecutionBlock:方法的操作是在不同的线程中异步执行的,并且不一定都在主线程。

并且我们还需注意:

  • addExecutionBlock:通常是把任务交给NSBlockOperation,调用它的线程不会执行该block。真正执行时由关联的NSOperationQueue调度,可能发生在后台,可能发生在主线程,取决于队列配置。
  • blockOperationWithBlock:是创建NSBlockOperation的类方法,并不会立即异步执行block。block会在该操作被加入队列并开始执行时运行,同样由队列决定执行线程。

因此,无论是哪个方法,它们只是定义或添加任务,任务实际在哪个线程执行取决于操作队列。加入并发队列时通常异步执行,加入主队列时则在主线程执行

start方法源码实现主要步骤:

  • 创建一个自动释放池,然后获取线程优先级。
  • 做一系列的状态异常判断,然后判断当前状态是否为isExecuting。
  • 如果是不是,手动变成isExecuting,然后判断当前任务是否被取消。
  • 若被取消就调用NSOperation的main方法。
  • 然后再调用NSOperation的finish方法。在finish方法内部通过KVO去变更isExecuting状态为isFinished状态。
  • 最后调用自动释放池的release。
  1. 使用自定义继承自NSOperation子类

自定义NSOperation子类主要分为非并发和并发两类。二者本质区别在于:

  • 非并发操作:只需要重写main方法,isExecuting(判断是否正在执行中)和isFinished(判断是否已经执行完)状态由底层NSOperation基类自动管理。当main方法执行完毕返回时,Operation会自动标记为已完成。适用于执行同步任务。
    注意:在main方法中,必须在执行关键人物前中后频繁判断isCancelled看操作是否被取消,以便及时响应外部cancel方法调用。
objc 复制代码
@implementation NonConcurrentOperation

- (void)main {
    // 在执行任务前先判断是否已经被取消
    if (self.isCancelled) {
        return;
    }
    
    NSLog(@"[Non-Concurrent] 开始同步任务: %@", [NSThread currentThread]);
    
    for (int i = 0; i < 2; i++) {
        // 每个循环或耗时步骤前,都需要检测取消状态
        if (self.isCancelled) {
            NSLog(@"[Non-Concurrent] 任务在循环中被取消");
            return;
        }
        
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"[Non-Concurrent] 正在执行步骤 %d --- %@", i + 1, [NSThread currentThread]);
    }
    
    NSLog(@"[Non-Concurrent] 同步任务自动结束完成");
    // main方法执行完毕,NSOperation会自动将isFinished设为YES
}

@end
  • 并发操作:必须重写start方法,不需要重写main方法。因为start方法触发异步任务会立即返回,此时任务没有真正结束,因此需要手动触发isExecuting和isFinished的KVO通知。
    注意:在start方法一开始就判断isCancelled,如果已经被取消,需立即将状态过渡到isFinished。
objc 复制代码
@interface ConcurrentOperation () {
    // 内部使用私有变量维护状态
    BOOL _executing;
    BOOL _finished;
}
@end

@implementation ConcurrentOperation

- (instancetype)init {
    self = [super init];
    if (self) {
        _executing = NO;
        _finished = NO;
    }
    return self;
}

// 声明此操作是异步执行的
- (BOOL)isAsynchronous {
    return YES;
}

- (BOOL)isExecuting {
    return _executing;
}

- (BOOL)isFinished {
    return _finished;
}

// 重写start方法
- (void)start {
    // 始终在最开始判断是否已被取消
    if (self.isCancelled) {
        [self completeOperation];
        return;
    }
    
    // 激活操作,更新状态并发送KVO通知
    [self willChangeValueForKey:@"isExecuting"];
    _executing = YES;
    [self didChangeValueForKey:@"isExecuting"];
    
    NSLog(@"[Concurrent] 开始异步任务: %@", [NSThread currentThread]);
    
    // 模拟异步网络请求或异步block派发
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [NSThread sleepForTimeInterval:2.0];
        // 异步回调回来后,依然要检查是否已被取消
        if (self.isCancelled) {
            [self completeOperation];
            return;
        }
        NSLog(@"[Concurrent] 异步网络请求数据成功返回!--- %@", [NSThread currentThread]);
        // 异步任务真正结束时,必须手动调用完成方法
        [self completeOperation];
    });
}

// 封装手动更新状态的辅助方法,必须严格发送KVO通知
- (void)completeOperation {
    [self willChangeValueForKey:@"isExecuting"];
    [self willChangeValueForKey:@"isFinished"];
    
    _executing = NO;
    _finished = YES;
    
    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isFinished"];
    
    NSLog(@"[Concurrent] 异步任务结束,手动置isFinished = YES");
}

@end

我们看一下两种操作的效果:

objc 复制代码
- (void)testCustomOperations {
    NSLog(@"非并发CustomOperation");
    
    // 直接调用start:会在当前线程阻塞执行
    NonConcurrentOperation *nonCop1 = [[NonConcurrentOperation alloc] init];
    [nonCop1 start];
    
    // 加入NSOperationQueue执行:会自动放入后台线程,不阻塞主线程
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    NonConcurrentOperation *nonCop2 = [[NonConcurrentOperation alloc] init];
    [queue addOperation:nonCop2];
    
    [NSThread sleepForTimeInterval:2.5];
    
    NSLog(@"并发CustomOperation");
    
    // 放入队列执行异步操作
    ConcurrentOperation *cop = [[ConcurrentOperation alloc] init];
    [queue addOperation:cop];
}
  • 非并发操作:
    • 直接调用start:在主线程调用,非并发会在当前主线程同步阻塞执行,会阻塞UI。
    • addOperation加入队列:NSOperationQueue自动分配了一个线程来执行操作任务,任务同步阻塞执行在后台线程,不影响主线程UI。
  • 并发操作:
    • 直接调用start:在主线程调用,立即返回,成功回调在线程2执行。
    • addOperation加入队列:NSOperationQueue自动分配一个线程执行,立即返回,随机回调在一个线程。

异步网络请求由系统调度,回调可能会发生在任何线程,具体由网络API和回调队列决定。且异步任务结束后,需手动更新状态

使用NSOperationQueue

NSOperationQueue共有两种队列:主队列和自定义队列。其中自定义队列包含了串行/并发功能。

  1. 创建队列
  • 主队列:添加到主队列中的操作都会放到主线程中执行。
objc 复制代码
NSOperationQueue *queue = [NSOperationQueue mainQueue];
  • 自定义队列:添加到自定义队列中的操作会放到子线程中执行,同时包含串行并发功能。
objc 复制代码
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
  1. 添加操作到队列
  • 先创建操作,再加入到队列中:
objc 复制代码
- (void)addOperation:(NSOperation *)op;
  • 无需先创建操作,在block中添加操作,直接将包含操作的block加入到队列中:
objc 复制代码
- (void)addOperationWithBlock:(void (^)(void))block;

无论是哪种,使用addOperation:添加操作,都会把操作交给NSOperationQueue调度,队列能够开启新线程,进行并发执行

  1. 控制串行并发执行

使用NSOperationQueue创建的自定义队列同时具有串行和并发执行的能力。其中maxConcurrentOperationCount属性用来控制一个特定队列中最多可以有多少个操作同时并发执行,称作最大并发操作数

注意:maxConcurrentOperationCount控制的不是并发线程的最大数,而是一个队列中同时能并发执行的操作最大数。且一个操作也不是只能在一个线程中运行

分三种情况:

  • maxConcurrentOperationCount:默认为-1,表示不进行限制,可进行并发执行。
  • maxConcurrentOperationCount:为1表示队列为串行队列,只能串行执行。
  • maxConcurrentOperationCount:大于1表示队列为并发队列,可以并发执行。

GCD使用信号量来设置并发数,NSOperationQueue通过设置maxConcurrentOperationCount来控制最大并发数。

objc 复制代码
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 2;

for (int i = 0; i < 5; i++) {
  [queue addOperationWithBlock:^{ 
      [NSThread sleepForTimeInterval:2];
      NSLog(@"%d-%@",i,[NSThread currentThread]);
  }];
}
  1. 添加依赖

通过依赖关系可以控制不同操作之间的执行顺序中,主要提供了三个API来供我们管理和查看依赖:

  • - (void)addDependency:(NSOperation *)op;:添加依赖,使当前操作依赖于操作op的完成。
  • - (void)removeDependency:(NSOperation *)op;:移除依赖,取消当前操作对操作op的依赖。
  • @property (readonly, copy) NSArray<NSOperation *> *dependencies; :当前操作开始执行前完成执行的所有操作对象数组。也就是当前操作要进行必须等待操作对象数组中所有操作执行完。
  1. 优先级

NSOperationQueue提供了queuePriority优先级属性,适用于同一操作队列中的操作,而不适用于不同操作队列中的操作。

默认所有新创建的操作对象优先级都是NSOperationQueuePriorityNormal,但我们可以通过setQueuePriority:方法来改变当前操作同一队列中的执行优先级。

优先级枚举值:

objc 复制代码
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
	NSOperationQueuePriorityVeryLow = -8L,
	NSOperationQueuePriorityLow = -4L,
	NSOperationQueuePriorityNormal = 0,
	NSOperationQueuePriorityHigh = 4,
	NSOperationQueuePriorityVeryHigh = 8
};

注意:NSOperationQueue设置优先级只会让CPU更高几率调用,而不一定优先级越高就一定优先全部完成。

对于添加到队列中的操作,分为未开始状态、准备就绪状态和开始执行状态。

  • 从未开始状态到就绪状态取决于操作之间的依赖关系。如op2依赖于op1,op1如果没有执行完,op2就无法走到就绪状态。
  • 从就绪状态到执行状态取决于操作之间相对优先级。如op1优先级如果高于op2,那么op1优先执行。但是由于各种原因,也有可能op2先完成。

总结一下:依赖关系决定能不能执行,优先级决定谁更有可能先被调用

  1. 线程间通信

UI刷新(如点击、滚动、拖拽等事件)一般在主线程里进行,而把一些耗时操作(如图片下载、文件上传等)放在其他线程。而当其他线程完成耗时操作,需要回到主线程,此时就需要线程间通信。

objc 复制代码
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 异步实现耗时操作
[queue addOperationWithBlock:^{
  NSLog(@"请求网络%@--%@", [NSOperationQueue currentQueue], [NSThread currentThread]);
  //回到主线程
  [[NSOperationQueue mainQueue] addOperationWithBlock:^{
    NSLog(@"刷新UI%@--%@", [NSOperationQueue currentQueue], [NSThread currentThread]);
  }];
}];
  1. 线程同步和安全

在多线程中如果一个线程在对临界区的资源进行读写操作,此时另一个线程也对同一片临界区的资源进行读写操作。那么如果第一个线程写完还没来得及读就被第二个线程进行了写的操作,那么第一个线程读取的资源就不是本来的资源而是被篡改后的,即数据竞争。此时两个线程就是不安全的。

线程安全通常需要线程同步来实现,线程同步又需要依赖锁来实现。

可借鉴笔者【iOS】源码学习-锁的原理这篇博客。

常用API

  • NSOperation:
    • 取消操作:本质是标记isCancelled状态。

      objc 复制代码
      - (void)cancel;
    • 判断操作状态方法:

      • - (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:
    • 取消、暂停、恢复操作:

      objc 复制代码
      - (void)cancelAllOperations; // 取消队列所有操作
      - (BOOL)isSuspended; // 判断队列是否处于暂停状态。YES为暂停状态,NO为恢复状态
      - (void)setSuspended:(BOOL)b; // 设置操作的暂停和恢复。YES表示暂停队列,NO表示恢复队列
    • 操作同步:阻塞当前线程,直到队列中的操作全部执行完毕。

      objc 复制代码
      - (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; 获取主队列。

总结

NSOperation与NSOperationQueue是对GCD面向对象的高级封装。具有任务抽象、队列调度、依赖管理、优先级控制、取消机制和线程同步等多功能。

相关推荐
二流小码农3 小时前
鸿蒙开发:实现文本渐变效果
android·ios·harmonyos
a4493153627 小时前
MacBook 转轴屏线断裂故障分析——eDP 排线更换与转轴维护
macos·电脑
WiresharkClub9 小时前
抓包遇到私有协议全是乱码?抓包鹰自定义协议解码教程
ios
奶糖 肥晨10 小时前
mac系统中Java项目环境配置与问题解决全记录|环境准备篇:JDK与Maven安装配置
java·macos·maven
带娃的IT创业者12 小时前
原生体验的回归:深入解析 TREK —— 专为 Apple Silicon 打造的轻量级容器方案
macos·docker·虚拟化·开源项目·容器化·apple silicon·trek
万里浮云12 小时前
Typora -mac版本永久免费
macos
奶糖 肥晨12 小时前
mac系统中Java项目环境配置与问题解决全记录| 项目构建篇:Maven编译与打包
java·macos·maven
其实防守也摸鱼15 小时前
KMP全栈开发:从Android到AI Agent的技术演进与实践
android·运维·网络·人工智能·学习·安全·macos
Lvan的前端笔记21 小时前
ios:版本号和Build构建号
ios