美文网首页iOS 底层原理 面试IOS开发知识点
OC底层原理二十八:Dispatch_source & Sync

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

作者: markhetao | 来源:发表于2020-11-09 16:53 被阅读0次

    OC底层原理 学习大纲

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

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

    准备工作:

    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 & Sync

        本文链接:https://www.haomeiwen.com/subject/dfrhbktx.html