OC底层原理二十八:Dispatch_source & Synchronized

OC底层原理 学习大纲

上节对源码进行了深耕,看官与作者都辛苦,本节较为轻松,主要分析dispatch_sourcesynchronized锁。

  1. dispatch_source源
  2. synchronized锁
  3. 面试题分析

准备工作:

  • 可编译的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的计时器与NSTimerCADisplayLink比较?

1. NSTimer

  • 存在延迟,与RunLoopRunLoop 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。因为它使用方便不用手动解锁。但是它是所有锁中最耗时的一种。
  • 我们先展示结论:
  1. @synchronized锁的对象很关键,它需要保障生命周期
    (因为被锁对象一旦不存在了,会导致解锁,失去锁内代码就不安全了。)

  2. @synchronized是一把递归互斥锁。锁的内部结构如下:

    image.png

  • 接下来我们从两个方面来分析@synchronized
  1. @synchronized的使用
  2. @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_enterobjc_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】 如果错误异常报错。如果正确,就快速缓存线程缓存中,便于下次查找

    其中【相应操作】包括三种状态:

    1. ACQUIRE 进行中: 当前线程任务加1更新相应数据
    2. RELEASE 释放中: 当前线程任务减1更新相应数据
    3. CHECK检查: 啥也不干

补充: 每个被锁的object对象拥有一个或多个线程
(我们寻找线程前,都需先判断当前线程的持有对象object是否与锁对象objec一致)

  • 其中fetch_cache函数,是进行缓存查询开辟的:

createNO: 仅查询
createYES查询开辟/扩容内存

image.png

2.2.2 objc_sync_exit 解锁
  • 搜索objc_sync_exit

    image.png

  • 如果对象存在id2data处理数据,类型为RELEASE,尝试解锁

  • 如果不存在啥也不干。(这次直接代码得懒得写了 )

id2data我们在上面已经分析过了。只是类型为RELEASE而已。

至此,我想你应该知道上述2个问题底层原理了。

Q1:为什么锁定对象self

  • 因为被锁对象不能提前释放,会触发解锁操作,锁内代码不安全。

  • 【补充】
    对象被释放时,调用objc_sync_enterobjc_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

验证:

  1. 打开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); // 信号量释放
        });
    }
}

你可能感兴趣的:(OC底层原理二十八:Dispatch_source & Synchronized)