iOS多线程-NSOperation

224 阅读2分钟

NSOperation

  • 是一个 抽象类 (The NSOperation class is an abstract class)

    无法直接使用,因为只有方法声明没有实现
    可使用它的子类:NSIvocationOperation、NSBlockOperation(较常用)

NSOperationQueue

  • 队列(默认并发)
  • 核心:将操作添加到队列里
  • 常用属性&方法
    • 最大并发数(队列最多以多少线程进行操作)
      queue.maxConcurrentOperationCount = 2;
    • 队列暂停&继续(队列暂停后可以添加任务进队列,但是不会执行)
      queue.suspended = YES;
    • 取消全部操作 [queue cancelAllOperations];

NSInvocationOperation

  • iOS2.0推出的
  • 基于NSTread封装
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self demo2];
}

-(void)demo2{
    NSInvocationOperation *io = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(demo:) object:@0];
    //没有添加到队列里,直接使用start则是同步操作,在当前线程执行,不开辟新线程
    [io start];
}

-(void)demo1{
    //1. 创建队列
    //NSOperationQueue是一个并发队列
    NSOperationQueue *queue = [NSOperationQueue new];
    
    for (int i = 0; i<5; i++) {
        //2. 创建操作
        //NSInvocationOperation默认是异步操作(任务)
        NSInvocationOperation *io = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(demo:) object:@(i)];
        //3. 将操作添加到队列里
        [queue addOperation:io];
    }
}

-(void)demo:(NSNumber *)num{
    NSLog(@"%@ %@",num,[NSThread currentThread]);
}

demo2中,调用start方法,操作会在 当前线程 执行

NSBlockOperation

  • iOS4.0推出的
  • 基于GCD封装
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self demo2];
}

-(void)demo3{
    NSBlockOperation *bk = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"%@",[NSThread currentThread]);
    }];
    
    [bk start];
}

//无法取到操作对象
//线程通信
-(void)demo2{
    //异步操作,并发队列
    [_queue addOperationWithBlock:^{//Block里是1个操作,在同个线程里执行
        for (int i=0; i<5; i++) {
            NSLog(@"假装在下载%d...%@",i,[NSThread currentThread]);
        }
        
        NSString *str = @"123";
        //主队列进行更新
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            NSLog(@"假装在更新%@...%@",str,[NSThread currentThread]);
        }];
    }];
}

//可以取到操作对象
-(void)demo1{
    //异步操作
    NSBlockOperation *bk1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"%@",[NSThread currentThread]);
        NSLog(@"%@",[NSThread currentThread]);
    }];
    
    NSBlockOperation *bk2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"%@",[NSThread currentThread]);
    }];
    
    //并行队列
    [_queue addOperation:bk1];
    [_queue addOperation:bk2];
}
  • 常用属性&方法
    • 服务质量
      op1.qualityOfService = NSQualityOfServiceUserInteractive;
    • 优先级
      op1.queuePriority = NSOperationQueuePriorityVeryHigh;
    • 添加依赖
      [op2 addDependency:op1];
    • 取消依赖
      [op2 removeDependency:op1];

异步下载图片+缓存管理

  • 要点:
    • 子线程下载图片, 主线程更新UI
    • 内存缓存池(NSMutableDictionary)
    • 操作缓存池(操作完成前需移除)
    • cell重用后操作刚好完成, 导致错误刷新问题(reloadRowsAtIndexPaths)
    • 沙盒缓存(1. 在内存缓存后判断 2. 沙盒缓存后写入内存缓存)
    • 内存警告处理(清除 图片&操作缓存池)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    MYTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"mycell" forIndexPath:indexPath];
    MYAppInfo *model = _appList[indexPath.row];
    cell.titleLbl.text = model.name;
    cell.detailLbl.text = model.download;
    
    //1.判断内存里是否有缓存图片,如果有则直接加载,提高加载效率,减少网络使用
    if (_ramImg[model.icon]) {
        NSLog(@"从内存加载");
        cell.iconView.image = _ramImg[model.icon];
        return cell;
    }
    
    UIImage *cellImg = [UIImage imageWithContentsOfFile:[model.icon cachesPath]];
    //2.判断沙盒里是否有缓存过的图片,有则从沙盒直接加载
    if (cellImg) {
        NSLog(@"从沙盒加载");
        cell.iconView.image = cellImg;
        [_ramImg setObject:cellImg forKey:model.icon];
        return cell;
    }
    
    //程序启动,没有内存和沙盒缓存的情况下,先给一个占位图,避免图片即使加载好,但是图片布局没有刷新,图片没有大小无法显示问题
    cell.iconView.image = [UIImage imageNamed:@"user_default"];
    
	//3.判断当前cell的图片是否已经有操作在执行,如果没有才新创建操作,有的话就不继续,解决延迟问题1
    if (_opList[model.icon]) {
        return cell;
    }
	//4.创建操作,从网络下载图片
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
		NSLog(@"从网络加载");
		if (indexPath.row > 11) {
			//模拟网络请求数据时延迟,此时来回滚动会出现两个问题
			//1.延迟时间较长,滚动tabelView就不断进入数据源方法,而此时图片数据在内存里没有缓存,就会不断进入创建操作的方法里,重复创建操作
			//2.cell重用问题,cell重用后之前的操作可能因为网络延迟,没有及时将刷新到之前的cell里,之前的cell就被推出后重用,此时刚好加载完之前的数据,重用cell的操作就会对当前cell进行刷新
			[NSThread sleepForTimeInterval:5];
		}
		NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:model.icon]];
        UIImage *iconImg = [UIImage imageWithData:data];
        //6.下载完成,线程通讯,回到主线程
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{ 
//            cell.iconView.image = iconImg;
			if (iconImg) {
				//7.将加载好的图片写入内存缓存池,避免推出的图片需要重新到网络进行加载
				[_ramImg setObject:iconImg forKey:model.icon];
				//让操作只刷新操作创建时的行数据,解决网络延迟问题2
				[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation: UITableViewRowAnimationNone];
				//8.数据写入到沙盒
				[data writeToFile:[model.icon cachesPath] atomically:YES];
			}
			//9.操作完成,将该操作从缓存池里移除,此时无论图片网络请求是否成功都需要移除,所以要放在判断后面,如果不移除,没有请求成功的图片就无法再进入操作里重新请求数据
			[_opList removeObjectForKey:model.icon];
		}];
    }];
    //5.将操作加入操作缓存池,对为完成的操作进行记录
    [_opList setObject:op forKey:model.icon];
    [_queue addOperation:op];
    
    return cell;
}

//解决内存警告,释放内存缓存
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    
    [_ramImg removeAllObjects];
    [_opList removeAllObjects];
}