- NSThread
- GCD
- NSOperation
一、多线程概念
1.1 同步/异步
- 同步
- 代码从上到下顺序执行
- 一个人依次执行任务,一次执行一个任务
- 异步
- 多个人可以同时执行多个任务
1.2 进程/线程
- 进程
- 系统中独立运行的一个应用程序
- 每个进程之间是独立的, 每个进程均运行在其专用的受保护的内存空间内
- 通过活动监视器可以查看Mac系统中所开启的进程
- 线程
- 一个进程由多个线程组成, 至少一个
- 线程是进程的基本执行单元, 一个进程中的所有任务都在线程中执行
1.3 多线程
-
一个进程可以开启多个线程, 多个线程可以"同时"执行不同的任务
-
多线程可以解决程序阻塞的问题
-
多线程可以提高程序的执行效率
1.3.1 多线程执行原理
- 单任务操作系统
- 只有进程, 没有线程
- 进程顺序执行
- 多任务操作系统
- 引入了线程
- 同一时间可以执行多个程序
- 单核CPU中同一时间CPU只能处理1个线程, 只有1个线程在执行
- CPU在线程之间切换,切换之前保存当前线程状态,切换到下一个线程的状态,调度线程的时间足够快, 多线程"同时"执行
- 如果线程数量非常多, CPU会在n个线程之间切换, 消耗大量的CPU资源
- 线程本身也占用一定资源
- 每个线程被调度的次数也会降低, 效率也就下去了
1.3.2 多线程优缺点
- 优点
- 能适当提高程序执行效率
- 能提高资源利用率(CPU, 内存)
- 线程上的任务执行完成后, 线程会自动销毁
- 缺点
- 开启线程占用一定的内存空间(默认情况下, 每个线程占用512KB)
- 如果开启大量的线程, 就会占用大量的内存, 降低程序的性能
- 线程越多, CPU在线程的开销就越大
- 程序设计更加复杂, 线程间的通信、多线程的数据共享
1.3.3 主线程
- 一个程序运行后, 默认会开启一个线程, 称为“主线程”或“UI主线程”
- 主线程一般用来刷新UI界面、处理UI事件(点击、滚动、拖拽)
- 主线程使用注意
- 别将耗时的操作放到主线程
- 耗时操作会阻塞主线程, 严重影响UI的流畅度, 给用户一种坏体验
1.3.4 何时使用
耗时操作
- 网络请求
- I/O
二、iOS中多线程的技术方案
- pthread
- POSIX 表示可移植操作系统接口(Protable Operating System Interface) pThread
- NSThread
- GCD
- NSOperation
2.1 pthread
#import <pthread.h>
pthread_t pthread;
NSString *name = @"lisi";
int result = pthread_create(&pthread, NULL, demo2, (__bridge void *)(name));
// 线程执行函数
void *demo2(void *param) {
NSString *name = (__bridge NSString *)(param);
NSLog(@"hello %@ %@", name, [NSThread currentThread]);
return NULL;
}
2.2 NSThread
// 方式1
NSThread *thread = [[NSThread alloc] initWithTarget: self selector:@selector(printHello) object:nil];
[thread start];
// 方式2
[NSThread detachNewThreadSelector:@selector(printHello) toTarget:self withObject:nil];
// 方式3
[self performSelectorInBackground:@selector(printHello:) withObject: @"lisi"];
2.2.1 线程状态
- 新建
- 就绪 (start) 进入可调度线程池 和其他线程一起等待被执行
- 运行 CPU调度 在不同线程之间切换执行
- 阻塞 从可调度线程池移出
- 死亡 自然执行完或 或 出错exit
2.2.2 线程属性
- name
- 设计线程名称可以在线程执行的方法内部出现异常的时候记录 异常和当前线程
- threadPriority
-
0到1 1表示优先级别最高
-
内核调度算法在决定运行哪个线程的时候, 会把线程的优先级作为考量因素, 较高的优先级的线程会比较低优先级的线程具有更多的运行机会。较高优先级不保证线程的具体执行时间先后, 只是相比较低优先级的线程, 它更有可能被调度器选择执行。
-
2.2.3 多线程访问共享资源的问题
- 共享资源
- 一个资源可能被多个线程共享, 即多个线程可能会访问同一块资源
- 多个线程访问同一个对象、同一个变量、同一个文件
- 当多个线程访问同一块资源的时候, 很容易引发数据错乱和数据安全问题
- 加锁后会影响程序的执行效率
- 线程安全
- 线程同时操作是不安全的, 多个线程同时操作一个全局变量
- 线程安全: 在多个线程进行读写操作的时候, 仍然能够保证数据的正确
- 主线程(UI线程)
- 几乎所有UIKit提供的类都是线程不安全的, 但所有更新UI的操作都放在主线程上进行, 这样就安全了
- 所有包含Mutable的类都是线程不安全的
- NSMutableString
- NSMutableArray
- NSMutableDictionary
2.2.4 互斥锁 @synchronized
- 能有效防止多线程抢夺资源造成的数据安全问题, 数据的正确性
- 线程同步, 顺序访问临界区域
- 锁的部分越少越好, 严格限制临界区域的语句
- 互斥锁原理:
- 每个对象内部都有一个锁(变量), 当有线程要进入sychronized到代码块中会先检查对象的锁是打开还是关闭状态, 默认锁是打开的, 如果线程执行到代码块内部, 会先上锁, 如果锁被关闭, 再有线程要执行代码块就先等待, 直到锁打开才可以进入。
- 加锁后程序的执行效率比不加锁的时候要低, 因为线程要等待锁, 但是锁保证了多个线程同时操作全局变量的安全性
@synchronized (对象) {
//读写操作
}
2.2.5 原子属性
-
属性中的修饰符
- nonatomic 非原子属性
- atomic 原子属性(线程安全), 针对多线程设计, 默认值, 保证同一时间只有一个线程能够写入(但同一时间多个线程都能取值)
- atomic 本身就有一把锁(自旋锁)
- 单写多读: 单个线程写入, 多个线程可以读取
-
nonatomic 和 atomic对比
- atomic: 线程安全, 需要消耗大量的资源
- nonatomic: 非线程安全, 适合内存小的移动设备
-
iOS开发的建议
- 所有属性都声明为nonatomic
- 尽量避免多线程抢夺同一块资源
- 尽量将加锁、资源抢夺的业务逻辑交给服务器端处理, 减小移动客户端的压力
2.2.6 互斥锁和自旋锁
- 互斥锁
- 如果发现其他线程正在执行锁定代码, 线程会进入休眠, 等其他线程时间片到打开锁后, 线程会被唤醒
- 自旋锁
- 如果发现有其他线程正在锁定代码, 线程会用死循环的方式, 一直等待锁定的代码执行完成, 自旋锁更适合执行不耗时的代码
2.2.7 weak和strong
- OC对象用strong
- 连线的UI对象用weak
- self.view.subviews.新增对象 用strong也不影响
2.2.8 自动释放池
-
iOS开发中的内存管理
- iOS开发中, 并没有JAVA或者C#中的垃圾回收机制(OC有垃圾回收)
- MRC中对象谁申请, 谁释放
- 使用ARC开发, 只是在编译的时候, 编译器会根据代码结构自动提添加retain、release和autorelease
-
自动释放池
- 标记为autorelease的对象, 会被添加到最近一次创建的自动释放池中
- 当自动释放池被销毁或耗尽的时候, 会向自动释放池中的所有对象发送release消息
- 每一次主线程消息循环的时候会创建自动释放池
- 事件机制是基于消息循环的
- 程序一直执行等待用户的输入是因为消息循环
- 消息循环在一个循环内部不断接收用户的输入
- 消息循环作用
- 程序不退出
- 处理用户的事件
- 运行循环结束前, 会释放自动释放池
-
主线程消息循环
- 程序启动开启主线程的消息循环
- 等待用户输入事件
- 创建Event
- 创建自动释放池
- 处理事件, 一些对象放入自动释放池中
- 一次循环结束前, 销毁自动释放池, 发送release消息
-
什么时候使用自动释放池
- 循环中创建大量临时变量, 循环内部创建
- 大循环中创建的临时变量无法及时回收
- 开启子线程需要创建子线程的自动释放池, 开始执行的时候
- 自线程无法访问主线程的自动释放池
- 循环中创建大量临时变量, 循环内部创建
2.2.9 属性修饰符
以下的为对属性的修饰符的讨论
- NSString 用copy
- 如果使用strong 原字符串和当前字符串指向一致, 一起变化
- copy对原来的字符串进行了拷贝操作
- block使用copy
- block MRC下捕获变量为栈block
- 如果block作为属性使用assign, 栈block赋值之后被清空, 访问野指针
- copy属性表示block在赋值的时候做了一次copy操作, 从栈block变为了堆block
- delegate使用weak
- 当前类 控制器的属性strong 当前类的delegate又设置为了self 循环引用
- 为防止这种情况, delegate属性会被设置为weak
- assign和weak
- assign 基本数据类型
- 如果assign修饰对象, 在栈上开辟一片空间存储新建对象在堆上的地址, 之后堆上的对象没有被强引用, 被释放 assign的内存地址不变 但指向的空间已经没有对象了 这个时候就会出现野指针错误
- weak 对象
- 如果新建对象赋值给weak, 在栈上开辟一片空间存储新建对象在堆上的地址, 之后堆上的对象没有被强引用, 被释放, weak会变为nil , 对这个weak发送消息不会发生任何事情
- objc_msgSend(消息接受者, 消息主体); 消息接收者为nil, 无法找到方法进行响应, 根据方法的返回值类型返回, 如果是对象返回nil, 如果返回值类型为指针, 指针大小为基本数据类型, 返回0, 返回值类型为结构体, 结构体字段用0填充
- 如果新建对象赋值给weak, 在栈上开辟一片空间存储新建对象在堆上的地址, 之后堆上的对象没有被强引用, 被释放, weak会变为nil , 对这个weak发送消息不会发生任何事情
- assign 基本数据类型
- MRC和ARC下都能使用assign, 只有ARC下才可以使用weak
- MRC下使用retain, ARC中使用strong
- MRC和ARC下都能用copy
2.2.10 消息循环
-
什么是消息循环
- Runloop就是消息循环, 每个线程内部都有一个消息循环
- 只有主线程的消息循环默认开启, 子线程的消息循环默认不开启
-
消息循环的目的
- 保证程序不退出
- 负责处理输入事件
- 如果没有事件发生, 会让程序进入休眠状态
-
输入事件
-
输入源 input source 触摸 键盘 自定义输入源
performSelector: onThread -
定时源 定时器 定时器执行的方法不易执行太耗时的操作, 否则会降低用户体验, 用户拖拽的时候会感到卡顿
-
-
消息循环模式
- NSDefaultRunLoopMode
- 使用最广的
- 处理 除了NSConnection 外的输入源
- NSRunLoopCommonModes
- 包括以下等等
- NSDefaultRunLoopMode
- UITrackingRunloopMode scrollview拖拽时消息模式发生变化
- 包括以下等等
- NSDefaultRunLoopMode
-
其他
- 消息循环模式必须和输入事件的模式匹配才会执行响应的事件
- 消息循环运行在某一种消息循环模式上, 没有指定就是默认
// 定时源加到当前runloop
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(demo) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
2.2.11 子线程的消息循环
- 主线程消息循环默认开启, 子线程消息循环不会开启
- 启动子线程的消息循环
[[NSRunLoop currentRunLoop] run]]
- 线程池, 在开启线程后永不销毁, 当需要让子线程执行新的方法, 使用performSelector让指定的方法在指定的子线程上运行
//在子线程添加输入源 开启runloop
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];
[thread start];
// 添加输入源
[self performSelector:@selector(demo1) onThread:thread withObject:nil waitUntilDone:NO];
// 子线程执行方法
- (void)demo {
NSLog(@"I'm running");
// run方式开启 消息循环一直执行 如果消息循环中没有设置输入事件 消息循环会立即结束
[[NSRunLoop currentRunLoop] run];
// 或者run同时设置自动关闭时间
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.0]];
// 如果设置了输入事件并且输入事件匹配当前runloop的mode 会等待下个输入事件不执行这里
NSLog(@"End");
}
// 执行在子线程的消息循环当中的输入源的方法
- (void)demo1 {
NSLog(@"I'm running on subThread runloop");
}
2.3 GCD
- Grand Central Dispatch
- 纯C语言, 提供了非常多强大的函数
- GCD的优点
- 苹果公司为多核的并行运算提出的解决方案
- GCD会自动利用更多的CPU内核
- GCD会自动管理线程的生命周期(创建、调度、销毁)
- 程序员只需要告诉GCD需要执行什么任务, 不需要写任何线程管理代码
- GCD管理着一个线程池, 已经执行完任务的线程会在线程中存在一段时间, 当线程池中存在可用线程, 会进行重用
2.3.1 任务和队列
- GCD核心
- 任务 方法
- 队列 用来存放任务
- GCD使用步骤
- 创建任务
- 将任务添加到队列中
- GCD会自动将队列中的任务取出, 放入对应的线程中执行
- 任务的取出遵循队列的FIFO原则: 先进先出
- 两个用来执行任务的函数
- 同步的方式执行
- 异步的方式执行
// 同步 全局队列 当前线程执行
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
NSLog(@"hello %@", [NSThread currentThread]);
});
2.3.2 队列
- 队列类型
- 并发队列 Concurrent Dispatch Queue
- 多个任务并发执行
- 只有在异步函数下才有效
- 串行队列 Serial Dispatch Queue
- 任务顺序执行
- 包括主队列, 也叫全局串行队列
- 并发队列 Concurrent Dispatch Queue
- 同步和异步决定了要不要开新的线程
- 同步, 在当前线程中执行, 不具备开启新的线程的能力
- 异步, 新的线程中执行, 能够开启新的线程
- 并发和串行决定了任务的执行方式
- 并发, 多个任务并发执行
- 串行, 顺序执行
2.3.3 串行队列
-
同步执行
- 不开启新线程
- 顺序执行队列内的任务
-
异步执行
- 开启一个新的线程
- 线程顺序执行队列中的任务
-
串行队列 同步 主队列进行同步会死锁
// 当前线程 顺序输出
dispatch_queue_t queue = dispatch_queue_create("yyh", DISPATCH_QUEUE_SERIAL);
for (int i = 0; i < 10; i++) {
// 同步
dispatch_sync(queue, ^{
NSLog(@"%d %@", i, [NSThread currentThread]);
});
}
- 串行队列 异步
// 只新开一个线程 顺序输出
dispatch_queue_t queue = dispatch_queue_create("yyh", DISPATCH_QUEUE_SERIAL);
for (int i = 0; i < 10; i++) {
// 同步
dispatch_async(queue, ^{
NSLog(@"%d %@", i, [NSThread currentThread]);
});
}
2.3.4 并发队列
- 同步执行
- 不开新线程
- 顺序执行队列内的任务
- 异步执行
-
能够开启多个线程
-
无序执行
-
- 并发队列 同步
// 当前线程 顺序输出
dispatch_queue_t queue = dispatch_queue_create("yyh", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 10; i++) {
dispatch_sync(queue, ^{
NSLog(@"%d %@", i, [NSThread currentThread]);
});
}
- 并发队列 异步
// 开启多个线程 无序输出
dispatch_queue_t queue = dispatch_queue_create("yyh", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 10; i++) {
dispatch_async(queue, ^{
NSLog(@"%d %@", i, [NSThread currentThread]);
});
}
2.3.5 主队列
- 主队列先执行完主线程上的代码, 才会执行队列中的任务
- 主队列 异步任务
- 不开线程, 同步执行
- 主队列特点: 如果主线程正在执行代码暂时不调度任务, 等主线程执行结束后再调度任务
- 主队列也叫全局串行队列
// 在主线程上 顺序执行
- (void)demo1 {
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"hello %d %@", i, [NSThread currentThread]);
});
}
}
- 主队列 同步执行
- 在主线程上 写这段代码 程序执行不出来(死锁)
- 死锁的原因, 当程序执行到下面这段代码时
- 主队列: 如果主线程正在执行代码, 就不调度任务
- 同步执行: 如果第一个任务没有执行, 就继续等待第一个任务执行完成, 再执行下一个任务
- 此时互相等待, 程序无法往下执行
// 出现错误 _dispatch_sync_f_slow
// 主队列在等待主线程将代码执行完(第一个任务执行完), 之后再执行调度任务
// 主线程遇到同步, 等待这一段代码执行完成
- (void)demo2 {
for (int i = 0; i < 10; i++) {
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"hello %d %@", i, [NSThread currentThread]);
});
}
}
- 主队列和串行队列区别
- 串行队列: 等待一个任务执行完成, 才会调度下一个任务
- 主队列: 先进先出调度队列, 如果主线程上有代码执行, 主队列不会调度任务
// 解决主队列串行死锁
// 将主队列同步代码 用异步新开线程包裹起来
// 同步执行放在了子线程 任务执行放到了主线程
- (void)demo3 {
// 全局队列异步执行
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"全局队列 异步执行%@", [NSThread currentThread]);
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"主队列 同步执行%@", [NSThread currentThread]);
});
});
NSLog(@"==");
}
2.3.5 全局队列
- 全局队列的本质就是并发队列
dispatch_get_global_queue(0,0);- 参数1 服务质量(优先级)
QOS_CLASS_DEFAULT - 参数2 flags 保留参数供未来使用, 传0避免返回值为NULL
- 执行效果和并行队列相同
- 全局队列和并发队列区别
- 并发队列有名称, 可以跟踪错误, 全局队列没有
- 在ARC中不需要考虑释放内存, dispatch_release(q); 不允许调用。在MRC中需要手动释放内存
- 并发队列create后需要release
- 全局队列不需要release, 一直存在
- 一般使用全局队列
2.3.6 同步任务
- 同步任务作用
- 按先后顺序执行
- 异步执行间的依赖关系
- 同步任务特点:
- 队列调度多个异步任务前, 指定一个同步任务, 让所有的异步任务都等待同步任务执行完, 这就是所谓的依赖关系
app下载, 验证密码, 扣费, 下载, 将验证密码的同步任务放到一个异步执行中去, 验证密码的同步任务会在子线程中执行, 不会阻塞UI
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
NSLog(@"输入密码 %@", [NSThread currentThread]);
});
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
NSLog(@"扣费%@", [NSThread currentThread]);
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"下载应用%@", [NSThread currentThread]);
});
});
- 各种队列执行效果 | |串行队列|主队列 | 并行队列| |:---: | :---: | :---: | :----: | |同步sync| | | |
| 一 |串行队列|并行队列|主队列| | --- | --- | --- | --- | | 同步执行 | | | |
| 串行队列 | 主队列 | 并行队列 | |
|---|---|---|---|
| 同步sync | 不开新线程顺序执行 | 主线程内死锁 | 不开新线程顺序执行 |
| 异步async | 开新线程顺序执行 | 就在主线程上顺序执行 | 开新线程并发执行 |
2.3.7 Barrier 栅栏函数
- 主要用于在多个异步操作完成之后, 统一对非线程安全的对象进行更新
- 适合大规模的I/O操作
- 当访问数据库或文件的时候, 更新数据的时候不能和其他更新或者读取的操作在统一时间执行, 可以使用调度组不过有点复杂, 可以使用dispatch_barrier_async解决
- 队列创建的时候必须显示指定为是并行队列, 否则无法起到阻塞的作用, 效果和dispatch_async一样
// 模拟下载多张图片 多线程中访问线程不安全对象
dispatch_queue_t queue = dispatch_queue_create("yyh", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSString *fileName = [NSString stringWithFormat:@"image/0%d.jpg", i % 10];
NSString *path = [[NSBundle mainBundle] pathForResource:fileName ofType:nil];
UIImage *image = [UIImage imageWithContentsOfFile:path];
NSLog(@"下载图片%d %@",i , [NSThread currentThread]);
dispatch_barrier_async(queue, ^{
[self.imageList addObject:image];
NSLog(@"保存图片%d %@",i , [NSThread currentThread]);
});
});
2.3.8 延迟执行和一次性执行
- 延迟执行
dispatch_after(dispatch_time_t when, dispatch_queue_t queue,
dispatch_block_t block);
- 一次性执行 线程安全的
// 在当前线程执行 通过静态全局onceToken判断 初始化为0 执行完为-1
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"hello ,%@",[NSThread currentThread]);
});
2.3.9 单例
- 工具类 不用每次都创建对象 程序中只存在一个实例
- 通过互斥锁保证线程安全和dispatch_once相比, 更加耗时, 前者是加锁释放锁, 后者是做简单的判断
// 单例不应该存在多个入口 以下仅仅用于比较
+ (instancetype)sharedNetworkTools{
static id instance = nil;
// 线程同步, 保证线程安全的
@synchronized (self) {
if (instance == nil) {
instance = [[self alloc] init];
}
}
return instance;;
}
+ (instancetype)sharedNetworkToolsWithOnce{
static id instance = nil;
// dispatch_once本身就是线程安全的
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (instance == nil) {
instance = [[self alloc] init];
}
});
return instance;
}
2.3.10 调度组
有时候需要在多个异步任务都执行完成之后继续做某些事情, 比如下载歌曲, 等所有歌曲下载完毕后, 主线程提示用户
// 演示调度组 监测任务数 采用group计数 加入任务时+1 执行完任务-1 任务数减为0的时候notify
- (void)demo1 {
// 创建组
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_async(group, queue, ^{
NSLog(@"正在下载第一个歌曲");
});
dispatch_group_async(group, queue, ^{
NSLog(@"正在下载第2个歌曲");
});
dispatch_group_async(group, queue, ^{
NSLog(@"正在下载第3歌曲");
});
// 运行的线程 取决于队列
// 三个异步任务执行完毕, 才执行
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"over, %@", [NSThread currentThread]);
});
}
调度组原理
- (void)demo2 {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"任务1");
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"任务2");
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"任务3");
dispatch_group_leave(group);
});
//等待组中的任务执行完毕
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"over");
});
//等待组中任务执行完毕才会执行后续代码
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"hello");
}
- 终端输入
man dispatch_group_async
2.4 NSOperation
- OC语言中基于GCD面向对象的封装
- 使用比GCD更简单
- 提供了GCD不好实现的功能
- 苹果推荐使用, 不用关心线程和线程的生命周期
- NSOperation头文件
- NSOperation是一个抽象类
- 不能直接使用(方法没有实现)
- 约束子类都有共同的属性和方法
- NSOperation的子类
- NSInvocationOperation
- NSBlockOperation
- 自定义operation
- NSOperation是一个抽象类
- NSOperationQueue
使用NSOperation和NSOperationQueue实现多线程的具体步骤
- 执行的操作封装到NSOperation对象中
- NSOperation对象添加到NSOperationQueue中
- 系统自动将NSOperationQueue中的NSOperation取出来
- 将取出的NSOpreation封装的操作放到一条新线程当中执行
2.4.1 NSInvocationOperation
- 创建NSInvocationOperation对象
- 调用start方法开始执行操作
- 更新操作的状态
- cancelled
- executing
- finished
- 调用操作的main方法
- 更新操作的状态
- 或者添加到队列 开启子线程
NSInvocationOperation * operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(demo1) object:nil];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:operation];
默认情况下调用start并不会开一条新线程去执行操作, 而是在当前线程同步执行操作, 只有将NSOperation放到NSOperationQueue中, 才会执行异步操作
2.4.2 NSBlockOperation
- 创建NSBlockOperation对象
- 通过
addExecutionBlock:方法添加更多操作 - 只要NSBlockOperation封装的操作数>1, 就会异步执行操作
// 直接start
- (void)demo1 {
NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"第1个 %@", [NSThread currentThread]);
}];
for (int i = 0; i < 10; i++) {
[bo addExecutionBlock:^{
NSLog(@"第%d个 %@", i, [NSThread currentThread]);
}];
}
[bo start];
}
// 操作添加到队列
- (void)demo2 {
NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"第1个 %@", [NSThread currentThread]);
}];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
for (int i = 0; i < 10; i++) {
[bo addExecutionBlock:^{
NSLog(@"第%d个 %@", i, [NSThread currentThread]);
}];
}
[queue addOperation:bo];
}
// block直接添加到queue
- (void)demo3 {
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
NSLog(@"第1个 %@", [NSThread currentThread]);
}];
}
// 操作的 completionBlock
- (void)demo5 {
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:2];
NSLog(@"hello %@", [NSThread currentThread]);
}];
// 这个block依然在子线程当中执行 不能设置UI相关
[op setCompletionBlock:^{
NSLog(@"操作完成 %@", [NSThread currentThread]);
}];
[self.myQueue addOperation:op];
}
2.4.3 NSOperationQueue
- NSOperationQueue作用
- NSOperation可以调用start方法执行任务, 但默认是同步执行的。如果将NSOperation添加到NSOperationQueue中, 系统会自动异步执行NSOperation中的操作
- 添加操作到NSOperationQueue中
addOperation:addOperationWithBlock:
2.4.5 线程间通信
- 主队列
- 添加到主线程的操作, 最终都执行在主线程上
[NSOperationQueue mainQueue]
- 当前队列
- 获取当前操作所在的队列
[NSOperationQueue currentQueue]
- 模拟当图片下载完成后回归到主线程上更新UI
// 异步下载图片
[self.myQueue addOperationWithBlock:^{
//回到主线程更新UI
NSLog(@"异步下载 %@", [NSThread currentThread]);
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"更新UI %@", [NSThread currentThread]);
}];
}];
2.4.6 NSOperation和GCD比较
- GCD
- iOS4.0推出, 主要对多核CPU做了优化, 是C语言的技术
- GCD是将任务(block)添加到队列(串行/并行/全局/主队列), 并且以同步/异步的方式执行任务的函数
- GCD提供了一些NSOperation不具备的功能
- 一次性执行 dispatch_once
- 延迟执行 dispatch_after
- 调度组 dispatch_group
- NSOperation
- iOS2.0推出, iOS4之后重写了NSOperation, 面向对象
- NSOperation将操作(异步的任务)添加到队列(并发队列), 就会执行指定操作的函数
- NSOperation里提供了方便的操作
- 最大并发数
- 队列的暂停/继续
- 取消所有的操作
- 指定操作之间的依赖关系(GCD可以同步实现)
2.4.7 最大并发数
-
什么是并发数 同时执行的任务数, 比如同时开3个线程执行3个任务, 并发数是3, 但是线程数量不一定等于同时执行的任务数, 一般来说会比这个任务数大, 约等于任务数
-
最大并发数相关方法
maxConcurrentOperationCountsetMaxConcurrentOperationCount:
-
执行的过程
- 把操作添加到队列
- 去线程池取空闲的线程, 如果没有就创建线程
- 把操作交给从线程池中取出的线程执行
- 执行完成后, 线程放回线程池
- 重复2,3,4直到所有的操作都做完
// 懒加载
- (NSOperationQueue *)queue{
if (_queue == nil) {
_queue = [NSOperationQueue new];
_queue.maxConcurrentOperationCount = 2;
}
return _queue;
}
for (int i = 0; i < 20; i++) {
[self.queue addOperationWithBlock:^{
[NSThread sleepForTimeInterval:2.0];
NSLog(@"i %@", [NSThread currentThread]);
}];
}
2.4.8 队列的暂停、取消、恢复
-
取消队列所有操作
cancelAllOperations- 正在执行的操作会执行完毕, 后续操作会取消
- 取消后队列移除剩余没有执行操作
-
暂停和恢复队列
setSupended:YES表示暂停, NO代表恢复队列- 当前正在执行的操作会执行完毕, 后续操作会暂停
- 没有执行的操作不会从队列中移除
isSuspended
2.4.9 操作在队列中你的优先级
- 设置NSOperation在queue中的优先级, 可以改变操作的执行优先级
queuePrioritysetQueuePriority
- iOS8以后推荐使用服务质量 qualityOfService
- 无法确保执行顺序严格依照优先级
typedef NS_ENUM(NSInteger, NSQualityOfService) {
NSQualityOfServiceUserInteractive = 0x21,
NSQualityOfServiceUserInitiated = 0x19,
NSQualityOfServiceUtility = 0x11,
NSQualityOfServiceBackground = 0x09,
NSQualityOfServiceDefault = -1
}
2.4.10 completionBlock
- 可以监听一个操作的执行完毕
setCompletionBlock:- 执行在子线程上
2.4.11 操作依赖
- NSOperation之间可以设置依赖保证执行顺序
- 比如一定要让A执行完后, 再去执行B
[operationB addDependency:operationA]//操作B依赖于操作A- 可以在不同的queue的NSOperation之间创建依赖关系
- 不能互相依赖 A依赖B B依赖A
NSBlockOperation *op0 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"下载 %@",[NSThread currentThread]);
}];
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:2.0];
NSLog(@"解压 %@",[NSThread currentThread]);
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"安装 %@",[NSThread currentThread]);
}];
[op2 addDependency:op1];
[op1 addDependency:op0];
[self.queue addOperations:@[op1, op0] waitUntilFinished:NO];
[[NSOperationQueue mainQueue] addOperation:op2];