iOS 底层原理 面试IOS开发知识点

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

2020-11-09  本文已影响0人  markhetao

OC底层原理 学习大纲

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

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

准备工作:

1. 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: 取消
- (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锁

  • 我们先展示结论:
  1. @synchronized锁的对象很关键,它需要保障生命周期
    (因为被锁对象一旦不存在了,会导致解锁,失去锁内代码就不安全了。)

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

    image.png
  1. @synchronized的使用
  2. @synchronized源码探究

2.1 @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

clang编译文件,也可以看到objc_sync_enterobjc_sync_exit

image.png image.png

objc_sync_enter处加断点,运行到此处时,

image.png image.png
再进入内部,可以看到代码是在libobjc.A.dylib库中:
image.png
2.2.1 objc_sync_enter 加锁

我们进入id2data

image.png

一共分为三步进行查找处理

  • 【第一步】如果支持快速缓存,就从快速缓存读取线程任务,进行相应操作返回

  • 【第二步】快速缓存没找到,就从线程缓存读取线程任务,进行相应操作返回

  • 【第三步】线程缓存也没找到,就循环遍历一个个线程任务,进行相应操作跳到done

  • 【Done】 如果错误异常报错。如果正确,就快速缓存线程缓存中,便于下次查找

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

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

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

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

image.png
2.2.2 objc_sync_exit 解锁

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

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

Q1:为什么锁定对象self

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

  • 【补充】
    对象被释放时,调用objc_sync_enterobjc_sync_exit底层代码显示:啥也不会做。这把已经完全失去作用了。

Q2:为什么@synchronized耗时严重?

  • 因为对象被锁后(比如self),该对象的所有操作,都变成了加锁操作,为了确保锁内代码安全,我们锁了对象(比如self)的所有操作

  • 最直接的影响是,被锁线程变多,执行操作时,查找线程查找任务都变得很耗时,而且每个被锁线程内的任务还是递归持有更耗时

  • 【补充】
    我们查询任务时,可能经历3次查询快速缓存查询->线程缓存查询->遍历所有线程查询),需要寻找线程匹配被锁对象nextData递归寻找任务。这些,就是耗时的点。
    (self需要处理的事务越多,占有的线程数threadCount和每个线程内的锁数量lockCount都会越多,查询也更耗时。)

😃 希望补充内容,可以让你回答得更为专业


3. 面试题分享

- (void)demo {
    
    NSLog(@"123");
    
    for (int i = 0; i < 20000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            self.dataSources = [NSMutableArray array];
        });
    }
}

验证:

  1. 打开Zombie Objects僵尸对象
  • 僵尸对象一种用来检测内存错误(EXC_BAD_ACCESS)的对象,它可以捕获任何对尝试访问坏内存调用

  • 如果给僵尸对象发送消息时,那么将在运行期间崩溃输出错误日志。通过日志可以定位野指针对象调用的方法类名

    image.png
    运行代码,错误日志显示:
    image.png
  • 调用[__NSArrayM release]时,是发送给了deallocated已析构释放的对象。验证了我们的猜想

- (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];
            }
        });
    }
}
- (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]; // 解锁
        });
    }
}
- (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); // 信号量释放
        });
    }
}
上一篇下一篇

猜你喜欢

热点阅读