iOS多线程(四)- NSOperation

915 阅读8分钟

一、NSOperation特性

1、灵活

2、自如

二、如何使用NSOperation

1、使用NSOperation分三步:创建操作、创建队列、把操作加入队列

NSOperation 是抽象类,我们只能使用其子类NSInvocationOperationNSBlockOperation

1.1、NSInvocationOperation
- (void)demo
{
    // 处理事务
    NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(handleInvocation:) object:@"hehe"];
    // 创建队列  ---  最重要的核心点
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    // 操作加入队列
    [queue addOperation:op];
}

-----打印:
   hehe --- <NSThread: 0x60000393a500>{number = 7, name = (null)}
- (void)demo1_1
{
    NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(handleInvocation:) object:@"123"];
    [op start];
}
------打印:
   123 --- <NSThread: 0x600003c90cc0>{number = 1, name = main}
- (void)demo1_2
{
    NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(handleInvocation:) object:@"123"];
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:op];
    [op start];
}
-----打印:
  *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSInvocationOperation start]: something is trying to start the receiver simultaneously from more than one thread'
-----原因:
    任务running中再次将任务start会造成混乱,所以最好不要直接start
- (void)demo1_3
{
    NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(handleInvocation:) object:@"123"];
    [[NSOperationQueue mainQueue] addOperation:op];
}
-----打印:
   123 --- <NSThread: 0x6000027c4cc0>{number = 1, name = main}
1.2、NSBlockOperation
- (void)demo2
{
    // 函数式编程block比target的好处:
    // 1、两种不同的timer实现比较 :block更精确,target有可能会收到runloop影响)
    // 2、代码可读性强 ---因为--- 业务逻辑和功能逻辑不分离
  
    //1:创建blockOperation -- 函数式编程
    NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"%@",[NSThread currentThread]);
        sleep(3);
    }];
    
    //1.1 添加执行代码块
    [bo addExecutionBlock:^{
        NSLog(@"这是一个执行代码块 - %@",[NSThread currentThread]);
    }];
    //1.2 设置监听
    bo.completionBlock = ^{
        NSLog(@"完成了!!!");
    };
    
    //2:创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    //3:添加到队列
    [queue addOperation:bo];
    NSLog(@"事务添加进了NSOperationQueue");
}

-----打印:
   事务添加进了NSOperationQueue
   <NSThread: 0x600000b4d580>{number = 4, name = (null)}
 	 这是一个执行代码块 - <NSThread: 0x600000b4e180>{number = 5, name = (null)}
	 完成了!!!
-----原因:
   NSOperation依赖队列执行,具有异步性,所以先执行了主线程的nslog
     
-----思考:
   为什么要 addExecutionBlock-添加任务
-----原因:
	 比gcd方便的地方----可控性。
	 gcd不能控制(例:取消)这个任务,但是NSOperation可以。
/**
 测试操作与队列的执行效果:异步并发
 */
- (void)demo3
{
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    for (int i = 0; i<20; i++) 
    {
        [queue addOperationWithBlock:^{
            NSLog(@"%@---%d",[NSThread currentThread],i);
        }];
    }
}

-----打印:
 <NSThread: 0x600001444d80>{number = 6, name = (null)}---0
 <NSThread: 0x600001446600>{number = 4, name = (null)}---1
 <NSThread: 0x60000147e800>{number = 7, name = (null)}---2
 <NSThread: 0x600001446580>{number = 3, name = (null)}---3
 <NSThread: 0x600001446600>{number = 4, name = (null)}---4
 <NSThread: 0x600001444d80>{number = 6, name = (null)}---5
 <NSThread: 0x600001446580>{number = 3, name = (null)}---6
 <NSThread: 0x600001446600>{number = 4, name = (null)}---7
 <NSThread: 0x600001444d80>{number = 6, name = (null)}---8
 <NSThread: 0x60000147e800>{number = 7, name = (null)}---9
 <NSThread: 0x600001446580>{number = 3, name = (null)}---10
 <NSThread: 0x600001446600>{number = 4, name = (null)}---11
 <NSThread: 0x60000147a780>{number = 8, name = (null)}---12  // 注意这里
 <NSThread: 0x60000147f740>{number = 10, name = (null)}---14 // 注意这里
 <NSThread: 0x600001477640>{number = 9, name = (null)}---13  // 注意这里
 <NSThread: 0x600001444d80>{number = 6, name = (null)}---15
 <NSThread: 0x600001446580>{number = 3, name = (null)}---16
 <NSThread: 0x60000147a780>{number = 8, name = (null)}---17
 <NSThread: 0x60000147a940>{number = 11, name = (null)}---18
 <NSThread: 0x600001477440>{number = 12, name = (null)}---19
/**
 优先级,只会让CPU有更高的几率调用,不是说设置高就一定全部先完成
 */
- (void)demo4
{
    // 优先级
    NSBlockOperation *bo1 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 5; i++) {
            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;
    
    //2:创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    //3:添加到队列 -- 先添加bo2再添加bo1
  	[queue addOperation:bo2];
    [queue addOperation:bo1];
}

-----打印:
 **第一个操作** 0 --- <NSThread: 0x600003ec0940>{number = 4, name = (null)}
 **第一个操作** 1 --- <NSThread: 0x600003ec0940>{number = 4, name = (null)}
 **第一个操作** 2 --- <NSThread: 0x600003ec0940>{number = 4, name = (null)}
 **第一个操作** 3 --- <NSThread: 0x600003ec0940>{number = 4, name = (null)}
 **第一个操作** 4 --- <NSThread: 0x600003ec0940>{number = 4, name = (null)}
 第二个操作 0 --- <NSThread: 0x600003ee8e00>{number = 7, name = (null)}
 第二个操作 1 --- <NSThread: 0x600003ee8e00>{number = 7, name = (null)}
 第二个操作 2 --- <NSThread: 0x600003ee8e00>{number = 7, name = (null)}
 第二个操作 3 --- <NSThread: 0x600003ee8e00>{number = 7, name = (null)}
 第二个操作 4 --- <NSThread: 0x600003ee8e00>{number = 7, name = (null)}
/**
 线程通讯
 */
- (void)demo5
{
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    queue.name = @"hehe";
    [queue addOperationWithBlock:^{
        NSLog(@"%@ = %@",[NSOperationQueue currentQueue],[NSThread currentThread]);
        //模拟请求网络
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            NSLog(@"%@ --%@",[NSOperationQueue currentQueue],[NSThread currentThread]);
        }];
    }];
}

-----打印:
 <NSOperationQueue: 0x7ffd78f05400>{name = 'hehe'} = <NSThread: 0x60000305bc00>{number = 5, name = (null)}
 <NSOperationQueue: 0x7ffd78e01da0>{name = 'NSOperationQueue Main Queue'} --<NSThread: 0x600003014a80>{number = 1, name = main}
- (void)demo6
{
    // 设置并发数
    // GCD 通过 设置信号量 来 控制并发数
    self.queue.name = @"hehe";
    self.queue.maxConcurrentOperationCount = 2;
    // self.queue + op
    for (int i = 0; i<10; i++) 
    {
        [self.queue addOperationWithBlock:^{ // 一个任务
            [NSThread sleepForTimeInterval:2];
            NSLog(@"%d-%@",i,[NSThread currentThread]);
        }];
    }
}

-----打印:
  这个没法演示,运行的情况就是每隔两秒打印两个nslog
  原因:每次执行两个任务,两个同时等了两秒后都执行完了,再执行后两个

2、设置依赖关系 --- 最重要

- (void)demo2
{
    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];

    [self.queue addOperations:@[bo1,bo2,bo3] waitUntilFinished:YES];
    
    NSLog(@"执行完了?我要干其他事");
}

-----打印:
 请求token
 拿着token,请求数据1
 拿着数据1,请求数据2
 执行完了?我要干其他事

3、关于operationQueue的挂起,继续,取消

- (void)demo1
{
    self.queue.name = @"hehe";
    self.queue.maxConcurrentOperationCount = 2;
    for (int i=0; i< 100; i++) 
    {
        [self.queue addOperationWithBlock:^{
            [NSThread sleepForTimeInterval:1.0];
            NSLog(@"%@-----%d",[NSThread currentThread],i);
        }];
    }
}

- (IBAction)pauseOrContinue:(id)sender
{
    if (self.queue.operationCount == 0) {
        NSLog(@"当前没有操作,没有必要挂起和继续");
        return;
    }
    // 正在执行的操作无法挂起
    if (self.queue.suspended) 
    {
        NSLog(@"当前是挂起状态,准备继续");
    }
  	else
    {
        NSLog(@"当前为执行状态,准备挂起");
    }
    self.queue.suspended = !self.queue.isSuspended;
}

- (IBAction)cancel:(id)sender 
{
    // 执行结果发现,正在执行的操作无法取消,因为这要回想到之前的NSThread
    // 取消操作之后,再点继续,发现没有调度的任务了 
    [self.queue cancelAllOperations]; // 将队列queue剩余任务全部舍弃
    // 若下次想执行取消前未执行的任务,就得重新添加任务
}

-----打印:
 <NSThread: 0x600000dd1d00>{number = 3, name = (null)}-----1
 <NSThread: 0x600000ddd7c0>{number = 4, name = (null)}-----0
 <NSThread: 0x600000ddd180>{number = 5, name = (null)}-----2
 <NSThread: 0x600000dbc740>{number = 6, name = (null)}-----3
 <NSThread: 0x600000ddd7c0>{number = 4, name = (null)}-----4
 <NSThread: 0x600000dbc740>{number = 6, name = (null)}-----5
 <NSThread: 0x600000ddd180>{number = 5, name = (null)}-----6
 <NSThread: 0x600000dbc740>{number = 6, name = (null)}-----7
 当前为执行状态,准备挂起
 <NSThread: 0x600000dd1d00>{number = 3, name = (null)}-----8
 <NSThread: 0x600000dbc740>{number = 6, name = (null)}-----9
-----提问:
   为何挂起后还有两次执行?
   答:因为挂起的时刻之前的两次任务已经被调度,将会在线程中执行---已经被调度的任务无法挂起

小插曲:

​ 队列中的任务一旦被调度,就离开了队列,然后等待cpu调度线程来执行,所产生的线程数与队列没有关系。主队列调度的任务由主线程执行,其他队列的任务由子线程执行。

三、NSOperation的应用

1、模仿一个最简单的SDWebImage

		// 内存缓存 --- 比沙盒快 --- 但内存会被清理
    UIImage *cacheImage = self.imageCacheDict[model.imageUrl];
    if (cacheImage)
    {
        NSLog(@"从内存获取image:%@",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];
    
    return cell;