iOS 多线程之 NSOperation

439 阅读9分钟

前序:按顺序阅读更好

概念篇-进程与线程,任务和队列

iOS 多线程之GCD

iOS 多线程之 NSOperation

iOS 多线程之方案对比

一 简介

NSOperation是对GCD的包装,GCD只支持FIFO的队列,而NSOpration可以设置最大并发数、设置优先级、添加依赖关系等调整执行顺序,NSOpration甚至可以跨队列设置依赖关系

NSOperatio有2个核心概念:NSOperation(操作)和NSOperationQueue(队列). NSOperation是个抽象类,依赖于子类NSInvocationOperation、NSBlockOperation去实现,另外还可以自定义NSOperation.

二 基本使用

1. NSOperationQueue

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

// 主队列获取方法
NSOperationQueue *queue = [NSOperationQueue mainQueue];

// 自定义队列创建方法
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

2. NSOperation

2.1 NSInvocationOperation

① 基本使用

- (void)invocationOperation {
   // 处理事务
   NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self
   selector:@selector(handleInvocation:) object:@"a"];
   // 创建队列
   NSOperationQueue *queue = [[NSOperationQueue alloc] init];
   // 操作加入队列
   [queue addOperation:op];
}

② 直接处理事务,不添加隐性队列

//在主线程单独使用子类 NSInvocationOperation
- (void)invocationOperation {
    NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(handleInvocation) object:@"a"];
    [op start];
}
-(void)handleInvocation{
   [NSThread sleepForTimeInterval:2];// 模拟耗时操作 
    NSLog(@"%d", [NSThread currentThread].isMainThread); // 打印当前线程
}
//在子线程使用子类 NSInvocationOperation
- (void)invocationOperation {
    [NSThread detachNewThreadSelector:@selector(useInvocationOperation) toTarget:self withObject:nil];
}
-(void)handleInvocation{
   [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"%d", [NSThread currentThread].isMainThread); // 打印当前线程
}
  • 如果不使用队列NSOperationQueue,单独使用子类NSOperation(NSInvocationOperation/NSBlockOperation/CustomOperation)执行一个操作的情况下, 操作是在当前线程执行的,并没有开启新线程(如果在主线程中执行,任务就在主线程;如果其他线程中执行,任务就在其他线程)。
  • 如果不使用队列NSOperationQueue,单独使用子类NSBlockOperation执行多个操作的情况下,如果在主线程中执行,只有第一个任务在主线程,其他的需要开启线程,开启的线程数是由系统来决定的;如果在子线程中执行,系统会根据操作的个数来决定是否开启新线程。
  • 如果结合队列NSOperationQueue使用NSOperation(NSInvocationOperation/NSBlockOperation),会自动开启新线程 ③ 错误使用
- (void)invocationOperation {
    NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(handleInvocation:) object:@"a"];
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:op];
    [op start];
}
--------------------错误日志:-------------------
something is trying to start the receiver simultaneously from more than one thread'
--------------------错误日志:-------------------

上述代码之所以会崩溃,是因为线程生命周期:

[queue addOperation:op]已经将处理事务的操作任务加入到队列中,并让线程运行 [op start]将已经运行的线程再次运行会造成线程混乱

2.2 NSBlockOperation

NSInvocationOperation和NSBlockOperation两者的区别在于: 前者类似target形式;后者类似block形式——函数式编程,业务逻辑代码可读性更高。

执行多个任务有两种写法:

  • addExecutionBlock方法往一个NSBlockOperation中追加多个任务,然后添加到一个NSOperationQueue中。
    • 额外操作(包括blockOperationWithBlock中的操作)可以在不同的线程中同时(并发)执行。completionBlock方法会在所有异步操作执行完后才执行,也就是只有当所有相关的操作已经完成执行时,才视为完成。
  • 定义多个NSBlockOperation,添加到一个NSOperationQueue中

2.2.1 第一种写法

- (void)blockOperation {
    // 初始化添加事务
    NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任务1————%@",[NSThread currentThread]);
    }];
    // 添加事务1
    [bo addExecutionBlock:^{
        NSLog(@"任务2————%@",[NSThread currentThread]);
    }];
    // 添加事务2
    [bo addExecutionBlock:^{
        NSLog(@"任务3————%@",[NSThread currentThread]);
    }];
    // 添加事务3
    [bo addExecutionBlock:^{
        NSLog(@"任务4————%@",[NSThread currentThread]);
    }];
    // 添加事务4
    [bo addExecutionBlock:^{
        NSLog(@"任务5————%@",[NSThread currentThread]);
    }];
    // 回调监听
    bo.completionBlock = ^{
        NSLog(@"completionBlock执行,任务全部完成");
    };
    
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:bo];
    NSLog(@"事务添加进了NSOperationQueue");
}
--------------------输出结果:-------------------
事务添加进了NSOperationQueue
任务1————<NSThread: 0x280be7d00>{number = 3, name = (null)}
任务4————<NSThread: 0x280be7d00>{number = 3, name = (null)}
任务3————<NSThread: 0x280bc0340>{number = 6, name = (null)}
任务2————<NSThread: 0x280bd8600>{number = 5, name = (null)}
任务5————<NSThread: 0x280be7d00>{number = 3, name = (null)}
completionBlock执行,任务全部完成
--------------------输出结果:-------------------

NSOperationQueue是异步执行的,所以任务12345的完成顺序不确定,但是completionBlock回调会在任务全部完成后执行。

2.2.2 第二种写法

- (void)blockOperation {
    // 初始化添加事务
    NSBlockOperation *bo1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任务1————%@",[NSThread currentThread]);
    }];
    NSBlockOperation *bo2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任务2————%@",[NSThread currentThread]);
    }];
    NSBlockOperation *bo3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任务3————%@",[NSThread currentThread]);
    }];
    NSBlockOperation *bo4 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任务4————%@",[NSThread currentThread]);
    }];
    // bo1的回调监听
    bo1.completionBlock = ^{
        NSLog(@"bo1 的completionBlock执行,任务完成");
    };
    // bo2的回调监听
    bo2.completionBlock = ^{
        NSLog(@"bo2 的completionBlock执行,任务完成");
    };
    // bo3的回调监听
    bo3.completionBlock = ^{
        NSLog(@"bo3 的completionBlock执行,任务完成");
    };
    // bo4的回调监听
    bo4.completionBlock = ^{
        NSLog(@"bo4 的completionBlock执行,任务完成");
    };
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:bo1];
    [queue addOperation:bo2];
    [queue addOperation:bo3];
    [queue addOperation:bo4];
    NSLog(@"事务添加进了NSOperationQueue");
}
--------------------输出结果:-------------------
事务添加进了NSOperationQueue
任务1————<NSThread: 0x2834dd200>{number = 7, name = (null)}
任务4————<NSThread: 0x2834d62c0>{number = 4, name = (null)}
任务2————<NSThread: 0x2834dd7c0>{number = 6, name = (null)}
任务3————<NSThread: 0x2834d6fc0>{number = 5, name = (null)}
bo4 的completionBlock执行,任务完成
bo1 的completionBlock执行,任务完成
bo3 的completionBlock执行,任务完成
bo2 的completionBlock执行,任务完成
--------------------输出结果:-------------------

3. NSOperation添加到NSOperationQueue中

  • - (void)addOperation:(NSOperation *)op; 需要先创建操作,再将创建好的操作加入到创建好的队列中去。
  • - (void)addOperationWithBlock:(void (^)(void))block; 无需先创建操作,在 block 中添加操作,直接将包含操作的 block 加入到队列中。 3.1 使用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]
}

3.2 使用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]); // 打印当前线程
        }
    }];
}

4. maxConcurrentOperationCount 最大并发数

通过设置NSOperationQueuemaxConcurrentOperationCount可以控制串行还是并发执行。

  • maxConcurrentOperationCount 默认情况下为-1,表示不进行限制,可进行并发执行。
  • maxConcurrentOperationCount 为1时,队列为串行队列。只能串行执行。
  • maxConcurrentOperationCount 大于1时,队列为并发队列。操作并发执行,真正执行的时候,并发数不会超过系统限制。

5. 添加依赖

在NSOperation中通过addDependency添加依赖能很好的控制任务执行的先后顺序

- (void)operationQueue {
    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];
    
    [self.queue addOperations:@[bo1,bo2,bo3] waitUntilFinished:YES];
    
    NSLog(@"全部执行完毕");
}

--------------------输出结果:-------------------
请求token
拿着token,请求数据1
拿着数据1,请求数据2
全部执行完毕
--------------------输出结果:-------------------

6. 设置优先级

NSOperation 提供了queuePriority(优先级)属性,queuePriority属性适用于同一操作队列中的操作,不适用于不同操作队列中的操作。默认情况下,所有新创建的操作对象优先级都是NSOperationQueuePriorityNormal。我们可以通过setQueuePriority:方法来改变当前操作在同一队列中的执行优先级。NSOperation设置优先级只会让CPU有更高的几率调用,不是说设置高就一定全部先完成。

- (void)blockOperation {
    NSBlockOperation *bo1 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 5; i++) {
            //sleep(1);
            NSLog(@"第一个操作 %d --- %@", i, [NSThread currentThread]);
        }
    }];
    // 设置最高优先级
    bo1.qualityOfService = NSQualityOfServiceUserInteractive;
    
    NSBlockOperation *bo2 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"第二个操作 %d --- %@", i, [NSThread currentThread]);
        }
    }];
    // 设置最低优先级
    bo2.qualityOfService = NSQualityOfServiceBackground;
    
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:bo1];
    [queue addOperation:bo2];
}
  • 不使用sleep——高优先级的任务1先于低优先级的任务2
  • 使用sleep进行延时——高优先级的任务1慢于低优先级的任务2

1、如果一个队列中既包含高优先级操作,又包含低优先级操作,并且两个操作都已经准备就绪,那么队列先执行高优先级操作。
2、优先级不能取代依赖关系。如果要控制操作间的启动顺序,则必须使用依赖关系。例如: 一个队列中包含operation1和operation2,operation1依赖operation2,但是operation1的优先级比operation2的优先级高, 结果是operation2优先执行。因为queuePriority属性决定的是进入准备就绪状态下的操作之间的开始执行顺序。

7. 任务的挂起、继续、取消

// 挂起
queue.suspended = YES;
// 继续
queue.suspended = NO;
// 取消
[queue cancelAllOperations];
  • 暂停操作不能使当前正在处于执行状态的任务暂停,而是该任务执行结束,后面的任务不会执行,处于排队等待状态。
  • 取消操作跟暂停相似,当前正在执行的任务不会立即取消,而是后面的所有任务永远不再执行,且该操作是不可以恢复的。

8. NSOperation和NSOperationQueue 常用属性和方法归纳

8.1 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; 在当前操作开始执行之前完成执行的所有操作对象数组。

8.2 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. 线程间通讯

在GCD中使用异步进行网络请求,然后回到主线程刷新UI.NSOperation中也有类似在线程间通讯的操作

- (void)operationQueue {
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    queue.name = @"test";
    [queue addOperationWithBlock:^{
        NSLog(@"请求网络%@--%@", [NSOperationQueue currentQueue], [NSThread currentThread]);
        
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            NSLog(@"刷新UI%@--%@", [NSOperationQueue currentQueue], [NSThread currentThread]);
        }];
    }];
}

2. 线程安全

模拟火车票售卖的方式,实现 NSOperation 线程安全和解决线程同步问题。
场景:总共有50张火车票,有两个售卖火车票的窗口,一个是北京火车票售卖窗口,另一个是上海火车票售卖窗口。两个窗口同时售卖火车票,卖完为止。

/**
 * 线程安全:使用 NSLock 加锁
 * 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
 */

- (void)initTicketStatusSave {
    NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程

    self.ticketSurplusCount = 50;

    self.lock = [[NSLock alloc] init];  // 初始化 NSLock 对象

    // 1.创建 queue1,queue1 代表北京火车票售卖窗口
    NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
    queue1.maxConcurrentOperationCount = 1;

    // 2.创建 queue2,queue2 代表上海火车票售卖窗口
    NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
    queue2.maxConcurrentOperationCount = 1;

    // 3.创建卖票操作 op1
    __weak typeof(self) weakSelf = self;
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        [weakSelf saleTicketSafe];
    }];

    // 4.创建卖票操作 op2
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        [weakSelf saleTicketSafe];
    }];

    // 5.添加操作,开始卖票
    [queue1 addOperation:op1];
    [queue2 addOperation:op2];
}

/**
 * 售卖火车票(线程安全)
 */
- (void)saleTicketSafe {
    while (1) {

        // 加锁
        [self.lock lock];

        if (self.ticketSurplusCount > 0) {
            //如果还有票,继续售卖
            self.ticketSurplusCount--;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        }

        // 解锁
        [self.lock unlock];

        if (self.ticketSurplusCount <= 0) {
            NSLog(@"所有火车票均已售完");
            break;
        }
    }
}

3. 自定义NSOperation缓存机制

根据SDWebImage加载网络图片的缓存机制,用NSOperation自定义图片缓存(本地缓存+内存缓存)

  • 如果内存中有数据,则从内存中取出图片来展示
  • 如果沙盒中有数据,则从沙盒中取出图片来展示并存一份到内存中
  • 如果都没有就异步下载把图片数据写到本地缓存和内存缓存中
-(void)simulationCacheImage{
    UIImage *cacheImage = self.imageCacheDict[model.imageUrl];
    if (cacheImage) {
        NSLog(@"从内存获取图片:%@", model.title);
        cell.imageView.image = cacheImage;
        return cell;
    }

    UIImage *diskImage = [UIImage imageWithContentsOfFile:[model.imageUrl getDowloadImagePath]];
    if (diskImage) {
        NSLog(@"从沙盒获取image:%@",model.title);
        cell.imageView.image = diskImage;
        [self.imageCacheDict setValue:diskImage forKey:model.imageUrl];
        return cell;
    }

    NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"去下载图片:%@", model.title);
        // 延迟
        NSData *data   = [NSData dataWithContentsOfURL:imageURL];
        UIImage *image = [UIImage imageWithData:data];
        // 存内存
        [self.imageCacheDict setValue:image forKey:model.imageUrl];
        [data writeToFile:[model.imageUrl getDowloadImagePath] atomically:YES];

        // 更新UI
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            cell.imageView.image = image;
        }];
    }];

    [self.queue addOperation:bo];
}