上节对源码进行了深耕,看官与作者都辛苦😂,本节较为轻松,主要分析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);
});
}
}
- 打印结果如下:
image.png
源的类型有很多,大家可以自行尝试。其中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锁的对象很关键,它需要保障锁的生命周期
(因为被锁对象一旦不存在了,会导致解锁,失去锁,锁内代码就不安全了。)
@synchronized是一把递归互斥锁。锁的内部结构如下:
image.png
- 接下来我们从两个方面来分析
@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:
image.png
- 可以看到
objc_sync_enter锁进入和objc_sync_exit锁退出关键函数。
clang编译文件,也可以看到
objc_sync_enter和objc_sync_exit:
image.png
image.png
在objc_sync_enter处加断点,运行到此处时,
image.png
- 运行到此处时
Ctrl + 鼠标左键点击进入内部:
Ctrl + 鼠标左键 点击
image.png
再进入内部,可以看到代码是在libobjc.A.dylib库中:
image.png
2.2.1 objc_sync_enter 加锁
- 进入
objc4源码,搜索objc_sync_enter,代码注释上标注,这是一个递归互斥锁。
image.png
- 如果
对象存在,id2data处理数据,类型为ACQUIRE,设置锁。 - 如果
不存在,啥也不干。
(内部:->BREAKPOINT_FUNCTION->调用asm("");就是啥也没干)
我们进入id2data:
image.png
一共分为
三步进行查找和处理:
【第一步】如果支持
快速缓存,就从快速缓存中读取线程和任务,进行相应操作并返回。【第二步】快速缓存
没找到,就从线程缓存中读取线程和任务,进行相应操作并返回。【第三步】线程缓存也
没找到,就循环遍历一个个线程和任务,进行相应操作并跳到done。【Done】 如果
错误:异常报错。如果正确,就存入快速缓存和线程缓存中,便于下次查找。其中【相应操作】包括三种状态:
ACQUIRE进行中: 当前线程内任务数加1,更新相应数据RELEASE释放中: 当前线程内任务数减1,更新相应数据CHECK检查: 啥也不干补充: 每个被锁的
object对象可拥有一个或多个线程。
(我们寻找线程前,都需先判断当前线程的持有对象object是否与锁对象objec一致)
- 其中
fetch_cache函数,是进行缓存查询和开辟的:
create为NO: 仅查询
create为YES:查询并开辟/扩容内存
image.png
2.2.2 objc_sync_exit 解锁
-
搜索
objc_sync_exit:
image.png
-
如果
对象存在,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); // 信号量释放
});
}
}

image.png
image.png
image.png
image.png
image.png
image.png












网友评论