iOS 多线程之NSOperation

在多线程的几个解决方案中,NSOperation 是非常有必要掌握的,因为相比较其他比如 GCD、NSThread 之类的,它要灵活的多,甚至可以自定义 NSOperation。

1.使用

理解操作与队列

NSOperation 是基于 GCD 的一层封装,对比 GCD,NSOperation 也有类似的 任务(NSOperation)队列(NSOperationQueue) 的概念。

  • 任务(NSOperation):在 NSOperation 中,任务是放在NSInvocationOperation(任务在 @selector 例)、NSBlockOperation(任务在 block 里) ,或自定义NSOperation来封装操作。三者都是通过继承 NSOperation 实现任务能力。
  • 队列(NSOperationQueue):把任务 添加到队列 即可开始执行任务。
    • 任务的执行顺序取决于它们的依赖关系优先级关系
    • 队列的串行或并发取决于设置的最大并发操作数(maxConcurrentOperationCount)

下例是 NSBlockOperation 加入执行队列

objc 复制代码
- (void)blockOperationTest{
    //1:创建blockOperation
    NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"%@",[NSThread currentThread]);
    }];
    //设置任务完成的监听
    bo.completionBlock = ^{
        NSLog(@"完成了!!!");
    };
    //2:创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    //3:添加到队列
    [queue addOperation:bo];   
}

NSInvocationOperation 加入执行队列

objc 复制代码
- (void)invocationOperationTest{
    //1:创建操作
    NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(handleInvocation:) object:@"123"];
    //2:创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    //3:操作加入队列 --- 操作会在新的线程中
    [queue addOperation:op];
}

- (void)handleInvocation:(id)op{
    NSLog(@"%@ --- %@",op,[NSThread currentThread]);
}

串行与并发

NSOperationQueue 有个关键属性 maxConcurrentOperationCount,叫做最大并发操作数。用来控制一个特定队列中可以有多少个操作同时参与并发执行。

注意:这里 maxConcurrentOperationCount 控制的不是并发线程的数量,而是一个队列中同时能并发执行的最大操作数。

  • maxConcurrentOperationCount默认情况下为-1,表示不进行限制,可进行并发执行。
  • maxConcurrentOperationCount为1时,队列为串行队列。只能串行执行。
  • maxConcurrentOperationCount大于1时,队列为并发队列。操作并发执行,当然这个值不应超过系统限制,即使自己设置一个很大的值,系统也会自动调整为 min。
objc 复制代码
- (void)testMaxConcurrent{
    self.queue.name = @"bird";
    self.queue.maxConcurrentOperationCount = 2;
    
    for (int i = 0; i<10; i++) {
        [self.queue addOperationWithBlock:^{
            [NSThread sleepForTimeInterval:1];
            NSLog(@"%d-%@",i,[NSThread currentThread]);
        }];
    }
}

结果如下

ini 复制代码
0-<NSThread: 0x600001287bc0>{number = 6, name = (null)}
1-<NSThread: 0x6000012db840>{number = 8, name = (null)}
等了1s
3-<NSThread: 0x600001287bc0>{number = 6, name = (null)}
2-<NSThread: 0x6000012df500>{number = 9, name = (null)}
等了1s
5-<NSThread: 0x600001287bc0>{number = 6, name = (null)}
4-<NSThread: 0x6000012df440>{number = 10, name = (null)}
等了1s
7-<NSThread: 0x600001287bc0>{number = 6, name = (null)}
6-<NSThread: 0x6000012df500>{number = 9, name = (null)}
等了1s
8-<NSThread: 0x6000012df440>{number = 10, name = (null)}
9-<NSThread: 0x600001287bc0>{number = 6, name = (null)}

可以看出maxConcurrentOperationCount控制的是最大操作数,而不是并发线程数量。

线程通信

objc 复制代码
- (void)testMainQueue {
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    queue.name = @"bird";
    [queue addOperationWithBlock:^{
        NSLog(@"%@ = %@",[NSOperationQueue currentQueue],[NSThread currentThread]);
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            NSLog(@"%@ --%@",[NSOperationQueue currentQueue],[NSThread currentThread]);
        }];
    }];
}

依赖(addDependency)

如前面所说,通过添加依赖addDependency:可以控制操作的执行顺序

objc 复制代码
- (void)addDependency {
    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];
    //注意这里一定不要构成循环依赖 : 不会报错,但是所有操作不会执行
    //[bo1 addDependency:bo3];
    //waitUntilFinished 堵塞线程
    [self.queue addOperations:@[bo1,bo2,bo3] waitUntilFinished:NO];
    
    NSLog(@"执行完了?我要干其他事");
}

结果如下,多次执行,结果都一样

ruby 复制代码
执行完了?我要干其他事
请求token
拿着token,请求数据1
拿着数据1,请求数据2

优先级

NSOperation 的优先级主要是用于帮助操作队列调度操作的执行顺序,但并不直接控制操作的具体执行时间点。

通过使用操作优先级,可以影响操作在队列中的执行次序,但实际执行时间点还受到多种因素的影响,比如操作之间的依赖关系、操作的耗时等,所以还需要考虑这些因素来正确地控制操作的执行顺序。

NSOperation 的 queuePriority 属性是一个枚举类型的属性,有以下几个选项:

objc 复制代码
// 优先级的取值
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};

以下是一个简单的示例代码,展示如何使用 NSOperation 的优先级属性

objc 复制代码
/**
 优先级,只会让CPU有更高的几率调用,不是说设置高就一定全部先完成
 */
- (void)demo4{
    NSBlockOperation *bo1 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 10; i++) {
            NSLog(@"第1个操作 %d --- %@", i, [NSThread currentThread]);
        }
    }];
    // 设置优先级 - 最高
    bo1.queuePriority = NSOperationQueuePriorityVeryHigh;
    
    //创建第二个操作
    NSBlockOperation *bo2 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 10; i++) {
            NSLog(@"第二个操作 %d --- %@", i, [NSThread currentThread]);
        }
    }];
    // 设置优先级 - 最低
    bo2.queuePriority = NSOperationQueuePriorityLow;
    
    //2:创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    //3:添加到队列
    [queue addOperation:bo1];
    [queue addOperation:bo2];
    
}

结果如下

ini 复制代码
第二个操作 0 --- <NSThread: 0x6000000c3980>{number = 6, name = (null)}
第1个操作 0 --- <NSThread: 0x6000000c1680>{number = 3, name = (null)}
第二个操作 1 --- <NSThread: 0x6000000c3980>{number = 6, name = (null)}
第1个操作 1 --- <NSThread: 0x6000000c1680>{number = 3, name = (null)}
第二个操作 2 --- <NSThread: 0x6000000c3980>{number = 6, name = (null)}
第1个操作 2 --- <NSThread: 0x6000000c1680>{number = 3, name = (null)}
第二个操作 3 --- <NSThread: 0x6000000c3980>{number = 6, name = (null)}
第1个操作 3 --- <NSThread: 0x6000000c1680>{number = 3, name = (null)}
第二个操作 4 --- <NSThread: 0x6000000c3980>{number = 6, name = (null)}
第1个操作 4 --- <NSThread: 0x6000000c1680>{number = 3, name = (null)}
第二个操作 5 --- <NSThread: 0x6000000c3980>{number = 6, name = (null)}
第1个操作 5 --- <NSThread: 0x6000000c1680>{number = 3, name = (null)}
第1个操作 6 --- <NSThread: 0x6000000c1680>{number = 3, name = (null)}
第二个操作 6 --- <NSThread: 0x6000000c3980>{number = 6, name = (null)}
第二个操作 7 --- <NSThread: 0x6000000c3980>{number = 6, name = (null)}
第1个操作 7 --- <NSThread: 0x6000000c1680>{number = 3, name = (null)}
第二个操作 8 --- <NSThread: 0x6000000c3980>{number = 6, name = (null)}
第1个操作 8 --- <NSThread: 0x6000000c1680>{number = 3, name = (null)}
第二个操作 9 --- <NSThread: 0x6000000c3980>{number = 6, name = (null)}
第1个操作 9 --- <NSThread: 0x6000000c1680>{number = 3, name = (null)}

可以看到第二个操作虽然优先级低,但也不一定就能先完成。

优先级不能替代依赖关系并且依赖关系高于优先级的设置,在保证依赖关系的情况下才会考虑优先级。

其他属性操作

  • 取消/暂停/恢复操作
    • - (void)cancelAllOperations; 可以取消队列的所有操作。
    • - (BOOL)isSuspended; 判断队列是否处于暂停状态。 YES 为暂停状态,NO 为恢复状态。
    • - (void)setSuspended:(BOOL)b; 可设置操作的暂停和恢复,YES 代表暂停队列,NO 代表恢复队列。
  • 操作同步
    • - (void)waitUntilAllOperationsAreFinished; 阻塞当前线程,直到队列中的操作全部执行完毕。也可以使用addOperations:waitUntilFinished:在添加操作到队列时设置。

线程安全

和 GCD 一样,就是通过给线程加锁实现线程安全。

2.自定义NSOperation

自定义 NSOperation 分为并发和非并发

  • 非并发:非并发很简单,重写 main 方法执行具体任务,当 main 方法结束,任务也就结束,不过因为非并发,应用起来意义不大。
objc 复制代码
// LFOperation.h 文件
#import <Foundation/Foundation.h>

@interface LFOperation : NSOperation

@end

// LFOperation.m 文件
#import "LFOperation.h"

@implementation YSCOperation

- (void)main {
    if (!self.isCancelled) {
        // 执行任务
    }
}

@end
  • 并发:并发需要重写下面几个方法或属性
    • start:把需要执行的任务放在start方法里,任务加到队列后,队列会管理任务并在线程被调度后,调用start方法,不需要调用父类的方法
    • asynchronous:表示是否并发执行
    • executing:表示任务是否正在执行,需要手动调用KVO方法来进行通知,方便其他类监听了任务的该属性
    • finished:表示任务是否结束,需要手动调用KVO方法来进行通知,队列也需要监听该属性的值,用于判断任务是否结束
objc 复制代码
// LFOperation.h 文件
#import <Foundation/Foundation.h>

@interface LFOperation : NSOperation
@property (nonatomic, readwrite, getter=isExecuting) BOOL executing; @property (nonatomic, readwrite, getter=isFinished) BOOL finished;
@end

// LFOperation.m 文件
#import "LFOperation.h"
// 因为父类的属性是Readonly的,重载时如果需要setter的话则需要手动合成。 
@synthesize executing = _executing;
@synthesize finished = _finished;

@implementation LFOperation

- (void)start {
    @autoreleasepool {
        @synchronized (self) {
            self.executing = YES;
            if (self.cancelled) {
                [self done];
                return;
            }
            // 任务。。。
        }
        // 任务执行完成,手动设置状态
        [self done];
    }
}

- (void)done {
    self.finished = YES;
    self.executing = NO;
}

#pragma mark - setter -- getter
// 需要手动调用 KVO 方法用来通知状态的变化
- (void)setExecuting:(BOOL)executing {
    //调用KVO通知
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    //调用KVO通知
    [self didChangeValueForKey:@"isExecuting"];
}

- (BOOL)isExecuting {
    return _executing;
}

- (void)setFinished:(BOOL)finished {
    if (_finished != finished) {
        [self willChangeValueForKey:@"isFinished"];
        _finished = finished;
        [self didChangeValueForKey:@"isFinished"];
    }
}

- (BOOL)isFinished {
    return _finished;
}

// 返回YES 标识为并发 Operation
- (BOOL)isAsynchronous {
    return YES;
}

@end

在Objective-C中,自动释放池(Autorelease Pool)用来管理临时对象的释放时机。在一个自动释放池的作用范围内,通过调用autorelease方法的对象将会在自动释放池被释放时自动调用release方法释放内存。

在平时的开发中,我们通常会在主线程中执行任务,而主线程会有一个默认的自动释放池。这意味着在主线程中创建的临时对象所使用的自动释放池会跟随主线程的生命周期。

但是,当我们在自定义的NSOperation中执行任务的时候,并不会自动创建一个自动释放池,这就意味着在start方法中执行的代码所创建的临时对象并不会被自动释放,而是会等到主线程的自动释放池被释放时才会被释放。

因此,在start方法中使用@autoreleasepool可以手动创建一个自动释放池,确保start方法执行期间创建的临时对象能够在合适的时候被及时释放,而不是等到主线程的生命周期结束才释放,因此使用@autoreleasepool是为了更好地控制内存的生命周期。

3. 知名开源库的NSOperation应用

SDWebImage的自定义NSOperation
YYImage的自定义NSOperation

相关推荐
iFlyCai2 小时前
Xcode 16 pod init失败的解决方案
ios·xcode·swift
郝晨妤11 小时前
HarmonyOS和OpenHarmony区别是什么?鸿蒙和安卓IOS的区别是什么?
android·ios·harmonyos·鸿蒙
Hgc5588866611 小时前
iOS 18.1,未公开的新功能
ios
CocoaKier13 小时前
苹果商店下载链接如何获取
ios·apple
zhlx283515 小时前
【免越狱】iOS砸壳 可下载AppStore任意版本 旧版本IPA下载
macos·ios·cocoa
XZHOUMIN1 天前
网易博客旧文----编译用于IOS的zlib版本
ios
爱吃香菇的小白菜1 天前
H5跳转App 判断App是否安装
前端·ios
二流小码农1 天前
鸿蒙开发:ForEach中为什么键值生成函数很重要
android·ios·harmonyos
hxx2211 天前
iOS swift开发--- 加载PDF文件并显示内容
ios·pdf·swift
B.-2 天前
在 Flutter 应用中调用后端接口的方法
android·flutter·http·ios·https