OC底层原理二十八:Dispatch_source & Sync
上节对源码
进行了深耕
,看官与作者都辛苦😂,本节较为轻松,主要分析dispatch_source
和synchronized
锁。
- dispatch_source源
- synchronized锁
- 面试题分析
准备工作:
- 可编译的
objc4-781
源码: https://www.jianshu.com/p/45dc31d91000
1. dispatch_source源
-
CPU负荷
非常小
,尽量不占资源
-
任何线程
调用它的函数dispatch_source_merge_data
后,会执行DispatchSource
事先定义好的句柄
(可以把句柄简单理解
为一个block
),这个过程叫custom event
,用户事件。是dispatch_source
支持处理的一种事件。
句柄
是一种指向指针的指针
。它指向
的是一个类
或结构
,它和系统有很密切的关系。
HINSTANCE
实例句柄、HBITMAP
位图句柄、HDC
设备表述句柄、HICON
图标句柄 等。其中还有一个通用句柄
,就是HANDLE
。
常用方法:
dispatch_source_create
:创建源dispatch_source_set_event_handler
: 设置源事件回调dispatch_source_merge_data
:置源事件设置数据dispatch_source_get_data
:获取源事件数据dispatch_resume
: 继续dispatch_suspend
: 挂起dispatch_cancel
: 取消
- 通过案例熟悉一下:
(源类型为DISPATCH_SOURCE_TYPE_DATA_ADD
)
- (void)viewDidLoad {
[super viewDidLoad];
__block NSInteger totalComplete = 0;
// 创建串行队列
dispatch_queue_t queue = dispatch_queue_create("ht", NULL);
// 创建主队列源,源类型为 DISPATCH_SOURCE_TYPE_DATA_ADD
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
// 设置源事件回调
dispatch_source_set_event_handler(source, ^{
NSLog(@"%@",[NSThread currentThread]);
NSUInteger value = dispatch_source_get_data(source);
totalComplete += value;
NSLog(@"进度: %.2f", totalComplete/100.0);
});
// 开启源事件
dispatch_resume(source);
// 发送数据源
for (int i= 0; i<100; i++) {
dispatch_async(queue, ^{
sleep(1);
// 发送源数据
dispatch_source_merge_data(source, 1);
});
}
}
- 打印结果如下:
源的类型有很多,大家可以自行尝试。其中DISPATCH_SOURCE_TYPE_TIMER计时器
使用很频繁:
//MARK: -ViewController
@interface ViewController ()
@property (nonatomic, strong) dispatch_source_t timer;
@property (nonatomic, strong) dispatch_queue_t queue;
@property (nonatomic, assign) double duration; // 总时长
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.duration = 10; // 总时长10秒
_queue = dispatch_queue_create("HT_dispatch_source_timer", DISPATCH_QUEUE_PRIORITY_DEFAULT);
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _queue);
// 从现在`DISPATCH_TIME_NOW`开始,每1秒执行一次
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0);
__block double currentDuration = self.duration;
__weak typeof(self) weakself = self;
dispatch_source_set_event_handler(_timer, ^{
dispatch_async(dispatch_get_main_queue(), ^{
if (currentDuration <= 0) {
NSLog(@"结束");
//取消
dispatch_cancel(weakself.timer);
return;
}
currentDuration--;
// 回到主线程,操作UI
NSLog(@"还需打印%.0f次",currentDuration + 1);
});
});
// 开始执行
dispatch_resume(_timer);
}
image.png
上述是一个最简单
的示例
,完整的计时器代码
,可在👉 这里下载
Q:
Dispatch_source_t
的计时器与NSTimer
、CADisplayLink
比较?1. NSTimer
存在延迟
,与RunLoop
和RunLoop Mode
有关
(如果Runloop
正在执行一个连续性运算,timer
会被延时触发
)- 需要手动
加入RunLoop
,且Model需要设置为forMode:NSCommonRunLoopMode
(NSDefaultRunLoopMode模式
,触摸事件会
让计时器暂停
)NSTimer *timer = [NSTimer timerWithTimeInterval:5 target:self selector:@selector(timerAction) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSCommonRunLoopMode];
2. CADisplayLink
屏幕刷新时
调用CADisplayLink
,以和屏幕刷新频率
同步的频率将特定内容
画在屏幕上
的定时器类。
CADisplayLink
以特定模式
注册到runloop
后,每当屏幕显示内容刷新结束
的时候,runloop
就会向CADisplayLink
指定的target
发送一次指定的selector
消息,CADisplayLink
类对应的selector
就会被调用一次
。所以通常情况下,按照iOS设备屏幕
的刷新率60次/秒
CADisplayLink
在正常情况下会在每次刷新结束
都被调用
,精确度
相当高
。
但如果调用的方法
比较耗时
,超过
了屏幕刷新周期
,就会
导致跳过
若干次回调调用机会
。
如果CPU
过于繁忙
,无法保证
屏幕60次/秒
的刷新率
,就会导致跳过
若干次调用回调方法
的机会,跳过
次数取决CPU
的忙碌程度
。3. dispatch_source_t 计时器
时间准确
,可以使用子线程
,解决
跑在主线程
上卡UI
的问题- 不依赖
runloop
,基于系统内核
进行处理,准确性
非常高
区别
NSTimer
会受到主线程
的任务的影响
,CADisplayLink
会受到CPU负载
的影响,产生延迟。dispatch_source_t
可以使用子线程
,而且可以使用leeway
参数指定
可以接受的误差
来降低
资源消耗
2. synchronized锁
- 各种类型
锁
的耗时比较
:
image.png 锁
,是为了确保线程安全
,数据
写入安全
。- 我们在开发中使用最多的,就是
@synchronized
。因为它使用方便
,不用
手动解锁
。但是它是所有锁中最耗时
的一种。
- 我们先展示结论:
@synchronized
锁的对象
很关键,它需要保障锁
的生命周期
(因为被锁对象
一旦不存在
了,会导致解锁
,失去锁
,锁内代码
就不安全了。)image.png
@synchronized
是一把递归互斥锁
。锁的内部结构如下:
- 接下来我们从两个方面来分析
@synchronized
:
-
@synchronized
的使用 -
@synchronized
源码探究
2.1 @synchronized
的使用
- 售票案例测试:
加入@synchronized
确保内部代码安全
(代码进入
时加锁
,代码离开
时移除锁
)
@interface ViewController ()
@property (nonatomic, assign) NSUInteger ticketCount;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.ticketCount = 20;
[self saleTicketDemo];
}
- (void)saleTicketDemo{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 3; i++) {
[self saleTicket];
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 10; i++) {
[self saleTicket];
}
});
}
- (void)saleTicket{
@synchronized (self) {
if (self.ticketCount > 0) {
self.ticketCount--;
sleep(0.1);
NSLog(@"当前余票还剩:%ld张",self.ticketCount);
}else{
NSLog(@"当前车票已售罄");
}
}
}
@end
image.png
Q1:为什么
锁定
对象写self
?
- 因为
被锁对象
不能提前释放
,会触发解锁
操作,锁内代码
不安全。Q2:为什么
@synchronized
耗时严重?
- 因为
对象被锁
后(比如self),该对象的所有操作
,都变成了加锁
操作,为了确保锁内代码安全
,我们锁了
对象(比如self)的所有操作
。- 最直接的影响是,
被锁线程
变多,执行
操作时,查找线程
和查找任务
都变得很耗时
,而且每个被锁线程
内的任务
还是递归持有
,更耗时
。
好了,结论
和原因
都解释清楚
了,应用层
知道这些就够了。
- 如果你不仅想
知其然
,还想知其所以然
,那么我们开始源码探究
:
2.2 @synchronized
源码探究
我们在@synchronized
代码处加入断点
,运行代码,打开Debug
->Debug Workflow
->Always show Disassemble
:
- 可以看到
objc_sync_enter
锁进入和objc_sync_exit
锁退出关键函数
。
clang编译文件,也可以看到
image.png image.pngobjc_sync_enter
和objc_sync_exit
:
在objc_sync_enter
处加断点,运行到此处时,
- 运行到此处时
Ctrl + 鼠标左键
点击进入内部
:
Ctrl + 鼠标左键 点击
再进入
内部,可以看到代码是在libobjc.A.dylib
库中:image.png
2.2.1 objc_sync_enter 加锁
- 进入
objc4
源码,搜索objc_sync_enter
,代码注释
上标注,这是一个递归互斥锁
。
image.png
- 如果
对象存在
,id2data
处理数据
,类型为ACQUIRE
,设置锁
。 - 如果
不存在
,啥也不干
。
(内部:->BREAKPOINT_FUNCTION
->调用asm("");
就是啥也没干)
我们进入id2data
:
一共分为
三步
进行查找
和处理
:
【第一步】如果支持
快速缓存
,就从快速缓存
中读取线程
和任务
,进行相应操作
并返回
。【第二步】快速缓存
没找到
,就从线程缓存
中读取线程
和任务
,进行相应操作
并返回
。【第三步】线程缓存也
没找到
,就循环遍历
一个个线程
和任务
,进行相应操作
并跳到done
。【Done】 如果
错误
:异常报错
。如果正确
,就存
入快速缓存
和线程缓存
中,便于
下次查找
。其中【相应操作】包括三种状态:
ACQUIRE
进行中: 当前线程
内任务
数加1
,更新
相应数据
RELEASE
释放中: 当前线程
内任务
数减1
,更新
相应数据
CHECK
检查: 啥也不干补充: 每个被锁的
object对象
可拥有
一个或多个线程
。
(我们寻找线程
前,都需先判断
当前线程的持有对象object
是否与锁对象objec
一致)
- 其中
fetch_cache
函数,是进行缓存查询
和开辟
的:
image.png
create
为NO
: 仅查询
create
为YES
:查询
并开辟
/扩容内存
2.2.2 objc_sync_exit 解锁
-
搜索
image.pngobjc_sync_exit
:
-
如果
对象存在
,id2data
处理数据,类型为RELEASE
,尝试解锁
。 -
如果
不存在
,啥也不干
。(这次直接代码得懒得写了 😂)
id2data
我们在上面已经分析过了。只是类型为RELEASE
而已。
至此,我想你应该知道上述2个问题
的底层原理
了。
Q1:为什么
锁定对象
写self
?
因为
被锁对象
不能提前释放
,会触发解锁
操作,锁内代码
不安全。【补充】
当对象被释放
时,调用objc_sync_enter
和objc_sync_exit
,底层代码
显示:啥也不会做
。这把锁
已经完全失去作用
了。Q2:为什么
@synchronized
耗时严重?
因为
对象被锁
后(比如self),该对象的所有操作
,都变成了加锁
操作,为了确保锁内代码安全
,我们锁了
对象(比如self)的所有操作
。最直接的影响是,
被锁线程
变多,执行
操作时,查找线程
和查找任务
都变得很耗时
,而且每个被锁线程
内的任务
还是递归持有
,更耗时
。【补充】
我们查询任务
时,可能经历3次查询
(快速缓存
查询->线程缓存
查询->遍历所有线程
查询),需要寻找线程
、匹配被锁对象
,nextData递归寻找任务
。这些,就是耗时的点。
(self
需要处理的事务越多
,占有的线程数
threadCount和每个线程
内的锁数量
lockCount都会越多
,查询也更耗时
。)😃 希望
补充内容
,可以让你回答
得更为专业
。
3. 面试题分享
- Q:
下面操作
造成crash
的原因?
- (void)demo {
NSLog(@"123");
for (int i = 0; i < 20000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.dataSources = [NSMutableArray array];
});
}
}
- A:触发
set
方法,set方法
本质是新值retain
,旧值release
。
dispatch_async
异步线程调用时,可能造成多次release
,过度释放
,形成野指针
。所以crash
。
验证:
- 打开
Zombie Objects
僵尸对象
僵尸对象
一种用来检测内存错误
(EXC_BAD_ACCESS
)的对象
,它可以捕获
任何对尝试访问坏内存
的调用
。如果给
image.png僵尸对象
发送消息
时,那么将在运行期间崩溃
和输出错误日志
。通过日志可以定位
到野指针对象
调用的方法
和类名
。
运行代码,错误日志显示:
image.png调用
[__NSArrayM release]
时,是发送给了deallocated已析构释放
的对象。验证
了我们的猜想
- 尝试1: 加入
@synchronized (self.dataSources)
锁:
- (void)demo {
NSLog(@"123");
self.dataSources = [NSMutableArray array];
for (int i = 0; i < 20000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (self.dataSources) { // 这是【错误实例】
self.dataSources = [NSMutableArray array];
}
});
}
}
发现还是
Crash
。是否知道原因
?你是【学会了】
还是【学废了】
😂
- 这个问题答案,就是本文
Q1问题
的答案
。- 因为
synchronized
锁的对象是self.dataSources
,它释放了
等于这把锁形同虚设
。
synchronized
锁的对象,需要确保锁内代码
的声明周期
。所以将锁对象
改为self
。就解决问题了。
- (void)demo {
NSLog(@"123");
self.dataSources = [NSMutableArray array];
for (int i = 0; i < 20000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (self) { // 这是【正确实例】但耗时高
self.dataSources = [NSMutableArray array];
}
});
}
}
- 可以使用其他锁来代替
@synchronized
,如:NSLock
- (void)demo {
NSLog(@"123");
self.dataSources = [NSMutableArray array];
NSLock * lock = [NSLock new]; // 创建
for (int i = 0; i < 20000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[lock lock]; // 加锁
self.dataSources = [NSMutableArray array];
[lock unlock]; // 解锁
});
}
}
- 使用
dispatch_semaphore
信号量:
- (void)demo {
NSLog(@"123");
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1); // 设置信号量(同时最多执行1个任务)
for (int i = 0; i < 20000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // 信号量等待
self.dataSources = [NSMutableArray array];
dispatch_semaphore_signal(semaphore); // 信号量释放
});
}
}