NSOperation 是iOS中比较高级的并发编程的操作!
NSOperation : 封装了 GCD 中的"任务"!
NSOperationQueue : 封装了 GCD 中的"队列"!
-
NSOperation
是一个"抽象类",不能直接使用 - 抽象类的用处是定义子类共有的属性和方法
- 在苹果的头文件中,有些抽象类和子类的定义是在同一个头文件中的
- 子类:
-
NSInvocationOperation
(使用和按钮/ target) -
NSBlockOperation
(利用block 封装任务)
-
-
NSOperationQueue
队列
其他基本的抽象类
UIGestureRecognizer
CAAnimation
CAPropertyAnimation
一. 基本演练
NSInvocationOperation
start
-
start
方法 会在当前线程执行@selector
方法
- (void)opDemo1 {
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(downloadImage:) object:@"Invocation"];
// start方法:直接开始,会在当前线程执行 @selector 方法
[op start];
}
- (void)downloadImage:(id)obj {
NSLog(@"%@ %@", [NSThread currentThread], obj);
}
添加到队列
- 将操作添加到队列,会"异步"执行
selector
方法
- (void)opDemo2 {
NSOperationQueue *q = [[NSOperationQueue alloc] init];
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(downloadImage:) object:@"queue"];
[q addOperation:op];
}
添加多个操作
- (void)opDemo3 {
NSOperationQueue *q = [[NSOperationQueue alloc] init];
for (int i = 0; i < 10; ++i) {
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(downloadImage:) object:@(i)];
[q addOperation:op];
}
}
执行效果:会开启多条线程,而且不是顺序执行。与GCD中并发队列&异步执行效果一样!
结论,在 NSOperation 中:
- 操作 -> 异步执行的任务
- 队列 -> 全局队列
NSBlockOperation
- (void)opDemo4 {
NSOperationQueue *q = [[NSOperationQueue alloc] init];
#warning 打印结果是在主线程中,说明直接在主线程执行,没有开辟新的线程
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@", [NSThread currentThread]);
}];
[q addOperation:op];
}
使用 block 来定义操作,所有的代码写在一起,更简单,便于维护!
更简单的,直接添加 Block
- (void)opDemo5 {
NSOperationQueue *q = [[NSOperationQueue alloc] init];
for (int i = 0; i < 10; ++i) {
[q addOperationWithBlock:^{
NSLog(@"%@ %d", [NSThread currentThread], i);
}];
}
}
向队列中添加不同的操作
- (void)opDemo5 {
NSOperationQueue *q = [[NSOperationQueue alloc] init];
for (int i = 0; i < 10; ++i) {
[q addOperationWithBlock:^{
NSLog(@"%@ %d", [NSThread currentThread], i);
}];
}
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"block %@", [NSThread currentThread]);
}];
[q addOperation:op1];
NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(downloadImage:) object:@"invocation"];
[q addOperation:op2];
}
- 可以向
NSOperationQueue
中添加任意NSOperation
的子类
线程间通讯
- (void)opDemo6 {
NSOperationQueue *q = [[NSOperationQueue alloc] init];
[q addOperationWithBlock:^{
NSLog(@"耗时操作 %@", [NSThread currentThread]);
// 主线程更新 UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"更新 UI %@", [NSThread currentThread]);
}];
}];
}
二. 高级演练
全局队列
/// 全局操作队列,统一管理所有的异步操作
@property (nonatomic, strong) NSOperationQueue *queue;
- (NSOperationQueue *)queue {
if (_queue == nil) {
_queue = [[NSOperationQueue alloc] init];
}
return _queue;
}
最大并发操作数
/// MARK: - 最大并发操作数
- (void)opDemo1 {
// 设置同时并发操作数
self.queue.maxConcurrentOperationCount = 2;
NSLog(@"start");
for (int i = 0; i < 10; ++i) {
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"%@ %d", [NSThread currentThread], i);
}];
[self.queue addOperation:op];
}
}
暂停 & 继续
/// MARK: - 暂停 & 继续
- (IBAction)pauseAndResume {
if (self.queue.operationCount == 0) {
NSLog(@"没有操作");
return;
}
// 暂停或者继续
self.queue.suspended = !self.queue.isSuspended;
if (self.queue.isSuspended) {
NSLog(@"暂停 %tu", self.queue.operationCount);
} else {
NSLog(@"继续 %tu", self.queue.operationCount);
}
}
- 队列挂起,当前"没有完成的操作",是包含在队列的操作数中的
- 队列挂起,不会影响已经执行操作的执行状态
- 对列一旦被挂起,再添加的操作不会被调度
取消全部操作
/// MARK: - 取消所有操作
- (IBAction)cancelAll {
if (self.queue.operationCount == 0) {
NSLog(@"没有操作");
return;
}
// 取消对列中的所有操作,同样不会影响到正在执行中的操作!
[self.queue cancelAllOperations];
NSLog(@"取消全部操作 %tu", self.queue.operationCount);
}
- 取消队列中所有的操作
- 不会取消正在执行中的操作
- 不会影响队列的挂起状态
依赖关系
#warning 不要添加循环操作依赖,一定要在添加进操作队列之前设置操作依赖
- (void)dependency {
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"登录 %@", [NSThread currentThread]);
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"付费 %@", [NSThread currentThread]);
}];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"下载 %@", [NSThread currentThread]);
}];
NSBlockOperation *op4 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"通知用户 %@", [NSThread currentThread]);
}];
[op2 addDependency:op1];
[op3 addDependency:op2];
[op4 addDependency:op3];
// 注意不要循环依赖
// [op1 addDependency:op4];
[self.queue addOperations:@[op1, op2, op3] waitUntilFinished:NO];
[[NSOperationQueue mainQueue] addOperation:op4];
NSLog(@"come here");
}
追加操作
当 NSBlockOperation 中的任务书 > 1 之后,无论是将操作添加到主线程,还是在主线程直接执行start, NSBlockOperation中的任务执行顺序都不确定,执行线程也不确定!
一般在开发中,要避免向NSBlockOperation 中追加任务
如果任务都是在主线程中执行,并且不需要保证执行顺序,可以直接追加任务
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 1. 实例化操作对象
NSBlockOperation *blockOperation1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"11111--->%@", [NSThread currentThread]);
}];
// 2. 往当前操作中追加操作一
[blockOperation1 addExecutionBlock:^{
NSLog(@"追加任务1--->%@", [NSThread currentThread]);
}];
// 2. 往当前操作中追加操作二
[blockOperation1 addExecutionBlock:^{
NSLog(@"追加任务2--->%@", [NSThread currentThread]);
}];
NSBlockOperation *blockOperation2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"22222--->%@", [NSThread currentThread]);
}];
NSBlockOperation *blockOperation3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"33333--->%@", [NSThread currentThread]);
}];
// 将操作添加到主队列中
// [[NSOperationQueue mainQueue] addOperation:blockOperation1];
// [[NSOperationQueue mainQueue] addOperation:blockOperation2];
// [[NSOperationQueue mainQueue] addOperation:blockOperation3];
/*
输出结果:
11111---><NSThread: 0x7fed62e065f0>{number = 1, name = main}
追加任务2---><NSThread: 0x7fed62e065f0>{number = 1, name = main}
追加任务1---><NSThread: 0x7fed62f07ba0>{number = 4, name = (null)}
22222---><NSThread: 0x7fed62e065f0>{number = 1, name = main}
33333---><NSThread: 0x7fed62e065f0>{number = 1, name = main}
*/
// 将操作添加到非主队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:blockOperation1];
[queue addOperation:blockOperation2];
[queue addOperation:blockOperation3];
/*
输出结果:
22222---><NSThread: 0x7fba18f08a20>{number = 9, name = (null)}
11111---><NSThread: 0x7fba18f03780>{number = 7, name = (null)}
33333---><NSThread: 0x7fba18d99c20>{number = 10, name = (null)}
追加任务1---><NSThread: 0x7fba18f08770>{number = 8, name = (null)}
追加任务2---><NSThread: 0x7fba18c06220>{number = 11, name = (null)}
*/
}
三. 与 GCD 的对比
-
GCD
- 将
任务(block)
添加到队列(串行/并发/主队列),并且指定任务执行的函数(同步/异步) - GCD是底层的C语言构成的API
- iOS 4.0 推出的,针对多核处理器的并发技术
- 在队列中执行的是由
block
构成的任务,这是一个轻量级的数据结构 - 要停止已经加入
queue
的block
需要写复杂的代码 - 需要通过
Barrier
或者同步任务设置任务之间的依赖关系 - 只能设置队列的优先级
- 高级功能:
- 一次性 once
- 延迟操作 after
- 调度组
- 将
-
NSOperation
- 核心概念:把
操作(异步)
添加到队列(全局的并发队列)
- OC 框架,更加面向对象,是对 GCD 的封装
- iOS 2.0 推出的,苹果推出 GCD 之后,对 NSOperation 的底层全部重写
-
Operation
作为一个对象,为我们提供了更多的选择 - 可以随时取消已经设定要准备执行的任务,已经执行的除外
- 可以跨队列设置操作的依赖关系
- 可以设置队列中每一个操作的优先级
- 高级功能:
- 最大操作并发数(GCD不好做)
- 继续/暂停/全部取消
- 跨队列设置操作的依赖关系
- 核心概念:把
四. 自定义操作
准备工作
- 自定义
DownloadImageOperation
继承自 NSOperation - 代码调用
// 实例化自定义操作
DownloadImageOperation *op = [[DownloadImageOperation alloc] init];
// 将自定义操作添加到下载队列
[self.downloadQueue addOperation:op];
需求驱动开发
目标一:设置自定义操作的执行入口
对于自定义操作,只要重写了
main
方法,当队列调度操作执行时,会自动运行main
方法
注意:为了能够及时释放内存,main
方法内部一般建立一个自动释放池,但是苹果官方文档不要求写!
- (void)main {
@autoreleasepool {
NSLog(@"main--->%@", [NSThread currentThread]);
NSString *filePath = @"http://ww1.sinaimg.cn/bmiddle/c260f7abjw1exxbbyckd6j20gn0m8wgh.jpg";
UIImage *image = [self downLoadImageWithStr:filePath];
// 回到主线程设置UI
[[NSOperationQueue mainQueue] addOperation:[NSBlockOperation blockOperationWithBlock:^{
NSLog(@"setupUI--->%@", [NSThread currentThread]);
self.imgView.image = image;
}]];
}
}
// 抽取出下载图片的方法
- (UIImage *)downLoadImageWithStr:(NSString *)filePath {
// 1. 转化为地址
NSURL *url = [NSURL URLWithString:filePath];
// 2. 转化为NSData 二进制类型数据(下载方法,耗时方法)
NSData *data = [NSData dataWithContentsOfURL:url];
// 3. 转化为图片类型
UIImage *image = [UIImage imageWithData:data];
NSLog(@"downLoadImage ---> %@", [NSThread currentThread]);
return image;
}
目标二:给自定义参数传递参数
- 定义属性
/** 承载图片的容器 */
@property (nonatomic, strong) UIImageView *imgView;
- 代码调用
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touchBegin--->%@", [NSThread currentThread]);
// 1. 实例化自定义操作对象
NSDownLoadOperation *operation = [[NSDownLoadOperation alloc] init];
// 2. 告诉操作在哪里显示图片
// 注意: 不能做以下改变 self.imageView = [[UIImageView alloc] init];
operation.imgView = self.imgView;
// 3.1 将自定义操作添加到下载队列,操作启动后会执行 main
[self.queue addOperation:operation];
// 3.2 直接开始
// 这样的话,全部操作都在当前线程(主线程)中进行操
//[operation start];
NSLog(@"touchEnd--->%@", [NSThread currentThread]);
}
注意,
main
方法被调用时,属性已经准备就绪
目标三:如何回调?
利用系统提供的 CompletionBlock
属性
// 设置完成回调
[operation setCompletionBlock:^{
NSLog(@"完成 %@", [NSThread currentThread]);
}];
- 只要设置了
CompletionBlock
,当操作执行完毕后,就会被自动调用 -
CompletionBlock
既不在主线程也不在操作执行所在线程 -
CompletionBlock
无法传递参数
自己定义回调 Block,在操作结束后执行
block: 指向结构体的指针!块代码!闭包!!
-
闭包: javascript/js最先使用 :可以从函数外部访问函数内部的变量! --- 灵活性!
- 定义 block 类型 (返回值、接受的参数类型...)
- 执行 block 中执行的内容(block 中封装的代码)
- 执行或调用 block (相当于调用函数)
----以上三个步骤,必须按顺序进行!
可以在不同的对象中,分别设置三个步骤,只要保证顺序就OK 定义属性
/// 完成回调 Block
@property (nonatomic, copy) void (^finishedBlock)(UIImage *image);
- 设置自定义回调
// 设置自定义完成回调
[op setFinishedBlock:^(UIImage *image) {
NSLog(@"finished %@ %@", [NSThread currentThread], image);
}];
- 耗时操作后执行回调
// 判断自定义回调是否存在
if (self.finishedBlock != nil) {
// 通常为了简化调用方的代码,异步操作结束后的回调,大多在主线程
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.finishedBlock(image对象);
}];
}
目标四:简化操作创建
- 定义
类
方法
/// 实例化下载图像操作
///
/// @param URLString 图像 URL 字符串
/// @param finished 完成回调 Block
///
/// @return 下载操作实例
+ (instancetype)downloadImageOperationWithURLString:(NSString *)URLString finished:(void (^)(UIImage *image))finished;
- 实现方法
+ (instancetype)downloadImageOperationWithURLString:(NSString *)URLString finished:(void (^)(UIImage *))finished {
DownloadImageOperation *operation = [[DownloadImageOperation alloc] init];
operation.URLString = URLString;
operation.finishedBlock = finished;
return operation;
}
- 方法调用
// 使用类方法实例化下载操作
DownloadImageOperation *operation = [DownloadImageOperation downloadImageOperationWithURLString:@"http://www.baidu.com/img/bdlogo.png" finished:^(UIImage *image) {
NSLog(@"%@", image);
}];
// 将自定义操作添加到下载队列,操作启动后会执行 main 方法
[self.downloadQueue addOperation:op];
目标五:取消操作
在关键节点添加
isCancelled
判断
- 添加多个下载操作
for (int i = 0; i < 10; ++i) {
NSString *urlString = [NSString stringWithFormat:@"http://www.xxx.com/%04d.png", i];
DownloadImageOperation *op = [DownloadImageOperation downloadImageOperationWithURLString:urlString finished:^(UIImage *image) {
NSLog(@"===> %@", image);
}];
// 将自定义操作添加到下载队列,操作启动后会执行 main 方法
[self.downloadQueue addOperation:op];
}
- 设置队列最大并发操作数
_downloadQueue.maxConcurrentOperationCount = 2;
- 内存警告时取消所有操作
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
[self.downloadQueue cancelAllOperations];
}
cancelAllOperations
会向队列中的所有操作发送Cancel
消息
- 调整
main
方法,在关键节点判断
- (void)main {
NSLog(@"%s", __FUNCTION__);
@autoreleasepool {
NSLog(@"下载图像 %@", self.URLString);
// 模拟延时
[NSThread sleepForTimeInterval:1.0];
if (self.isCancelled) {
NSLog(@"1.--- 返回");
return;
}
// 判断自定义回调是否存在
if (self.finishedBlock != nil) {
// 通常为了简化调用方的代码,异步操作结束后的回调,大多在主线程
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.finishedBlock(self.URLString);
}];
}
}
}
注意:如果操作状态已经是
Cancel
,则不会执行main
函数