22-探究iOS底层原理|多线程技术【原子锁atomic、gcd Timer、NSTimer、CADisplayLink】

前言

之前,我们在探索动画及渲染相关原理的时候,我们输出了几篇文章,解答了iOS动画是如何渲染,特效是如何工作的疑惑。我们深感系统设计者在创作这些系统框架的时候,是如此脑洞大开,也 深深意识到了解一门技术的底层原理对于从事该方面工作的重要性。

因此我们决定 进一步探究iOS底层原理的任务。继上一篇文章了解了【iOS中的10个线程锁,与线程锁类型:自旋锁、互斥锁、递归锁】 探索之后,本篇文章将继续对GCD多线程底层原理的探索

一、原子锁atomic

1. atomic

atomic用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁

原子性:原子即为最小的物理单位,意味不可再分割;即代码都为一个整体在同一线程进行操作

atomic只是保证setter、getter是线程安全的,并不能保证使用属性的过程是线程安全的

2. 从源码分析getter和setter对于atomic的使用

我们在objc4中的objc-accessors.mm中找到对应的getter和setter的实现

getter的实现

// getter
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    if (offset == 0) {
        return object_getClass(self);
    }

    // Retain release world
    id *slot = (id*) ((char*)self + offset);
    if (!atomic) return *slot;
        
    // Atomic retain release world
    spinlock_t& slotlock = PropertyLocks[slot];
    slotlock.lock();
    id value = objc_retain(*slot);
    slotlock.unlock();
    
    // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
    return objc_autoreleaseReturnValue(value);
}

setter的实现

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}

从源码可以看出只有automic的属性才会进行加锁操作

二、iOS中的读写安全方案 线程锁的选择

思考以下场景,怎么做最合适

  • 同一时间,只能有1个线程进行写的操作
  • 同一时间,允许有多个线程进行读的操作
  • 同一时间,不允许既有写的操作,又有读的操作

上面的场景就是典型的“多读单写”,经常用于文件等数据的读写操作,iOS中的实现方案有以下两个

  • pthread_rwlock:读写锁
  • dispatch_barrier_async:异步栅栏调用

1. pthread_rwlock

pthread_rwlock是专用于读写文件的锁,其本质也是互斥锁,等待锁的线程会进入休眠

使用代码如下

@interface ViewController ()
@property (assign, nonatomic) pthread_rwlock_t lock;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 初始化锁
    pthread_rwlock_init(&_lock, NULL);
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    for (int i = 0; i < 10; i++) {
        dispatch_async(queue, ^{
            [self read];
        });
        dispatch_async(queue, ^{
            [self write];
        });
    }
}


- (void)read {
    // 加上读取数据的锁
    pthread_rwlock_rdlock(&_lock);
    
    sleep(1);
    NSLog(@"%s", __func__);
    
    pthread_rwlock_unlock(&_lock);
}

- (void)write
{
    // 加上写入数据的锁
    pthread_rwlock_wrlock(&_lock);
    
    sleep(1);
    NSLog(@"%s", __func__);
    
    pthread_rwlock_unlock(&_lock);
}

- (void)dealloc
{
    // 释放时要销毁锁
    pthread_rwlock_destroy(&_lock);
}


@end

2. dispatch_barrier_async

dispatch_barrier_async也叫栅栏函数,意在用于拦截多线程异步并发操作,只保证同时有一条线程在操作

用栅栏函数也可以保证多读单写的操作

使用代码如下

@interface ViewController ()
@property (strong, nonatomic) dispatch_queue_t queue;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
    
    for (int i = 0; i < 10; i++) {
        dispatch_async(self.queue, ^{
            [self read];
        });
        
        dispatch_async(self.queue, ^{
            [self read];
        });
        
        dispatch_async(self.queue, ^{
            [self read];
        });
        
        // 这个函数传入的并发队列必须是自己通过dispatch_queue_cretate创建的
        dispatch_barrier_async(self.queue, ^{
            [self write];
        });
    }
}


- (void)read {
    sleep(1);
    NSLog(@"read");
}

- (void)write
{
    sleep(1);
    NSLog(@"write");
}

@end

三、定时器

我们日常使用的定时器有以下几个:

  • CADisplayLink
  • NSTimer
  • GCD定时器

1. CADisplayLink

CADisplayLink是用于同步屏幕刷新频率的定时器

1.1 CADisplayLink和NSTimer的区别

  • iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高
  • NSTimer的精确度就显得低了点,比如NSTimer的触发时间到的时候,runloop如果在阻塞状态,触发时间就会推迟到下一个runloop周期。并且NSTimer新增了tolerance属性,让用户可以设置可以容忍的触发的时间的延迟范围
  • CADisplayLink使用场合相对专一,适合做UI的不停重绘,比如自定义动画引擎或者视频播放的渲染。NSTimer的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。在UI相关的动画或者显示内容使用CADisplayLink比起用NSTimer的好处就是我们不需要在格外关心屏幕的刷新频率了,因为它本身就是跟屏幕刷新同步的。

1.2 CADisplayLink在使用中会出现的循环引用问题

CADisplayLink在日常使用中,可能会出现循环引用问题,见示例代码

@interface ViewController ()
@property (strong, nonatomic) CADisplayLink *link;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
    [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}

- (void)linkTest
{
    NSLog(@"%s", __func__);
}

- (void)dealloc
{
    NSLog(@"%s", __func__);
    [self.link invalidate];
}

@end

由于ViewController里有个link属性指向这CADisplayLink对象CADisplayLink对象里的target又指向着ViewController里的linkTest,都是强引用,所以会造成循环引用,无法释放

1.3 解决方案

增加第三个对象,通过第三个对象将target调用的方法转发出去,具体如下图所示

实现代码如下

@interface HPProxy : NSObject

+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end

@implementation HPProxy

+ (instancetype)proxyWithTarget:(id)target
{
    LLProxy *proxy = [[LLProxy alloc] init];
    proxy.target = target;
    return proxy;
}

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return self.target;
}

@end

// ViewController.m文件中
#import "ViewController.h"
#import "HPProxy.h"

@interface ViewController ()
@property (strong, nonatomic) CADisplayLink *link;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.link = [CADisplayLink displayLinkWithTarget:[HPProxy proxyWithTarget:self] selector:@selector(linkTest)];
    [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}

- (void)linkTest
{
    NSLog(@"%s", __func__);
}

- (void)dealloc
{
    NSLog(@"%s", __func__);
    [self.link invalidate];
}

@end

2. NSTimer

NSTimer也是定时器,相比CADisplayLink使用范围更广,更灵活,但精确度会低一些

2.1 NSTimer在使用中会出现的循环引用问题

NSTimer在使用时也会存在循环引用问题,同CADisplayLink

@interface ViewController ()

@property (strong, nonatomic) NSTimer *timer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}

- (void)timerTest
{
    NSLog(@"%s", __func__);
}


- (void)dealloc
{
    NSLog(@"%s", __func__);
    
    [self.timer invalidate];
}

@end

2.2 解决方案

【第一种】借助第三对象并将方法转发,同CADisplayLink

@interface ViewController ()

@property (strong, nonatomic) NSTimer *timer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[HPProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];    
}

- (void)timerTest
{
    NSLog(@"%s", __func__);
}


- (void)dealloc
{
    NSLog(@"%s", __func__);
    
    [self.timer invalidate];
}

@end

【第二种】使用NSTimerblock回调来调用方法,并将self改为弱指针

__weak typeof(self) weakSelf = self;

self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
   [weakSelf timerTest];
}];

3. 统一优化方案

3.1 NSProxy

NSProxy是唯一一个没有继承自NSObject的类,它是专门用来做消息转发的

3.2 特点

  • 不继承NSObject,也是基类类型
  • 没有init方法,直接用alloc方法来初始化
  • 没有forwardingTargetForSelector方法,只支持消息转发

3.3 优化方案

HPProxy继承自NSProxy,然后在消息转发里替换target

这么做的好处在于NSProxy相比NSObject少了消息发送先从父类查找的过程,以及不经过forwardingTargetForSelector,相比之下性能会高

替换代码如下

@interface HPProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end

@implementation LLProxy

+ (instancetype)proxyWithTarget:(id)target
{
    // NSProxy对象不需要调用init,因为它本来就没有init方法
    LLProxy *proxy = [LLProxy alloc];
    proxy.target = target;
    return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    [invocation invokeWithTarget:self.target];
}
@end

3.4 从源码实现来分析

我们先查看下面这句代码打印什么

ViewController *vc = [[ViewController alloc] init];
HPProxy *proxy = [HPProxy proxyWithTarget:vc];
   
NSLog(@"%d, [proxy isKindOfClass:[ViewController class]]);

打印结果为1,可以看出NSProxyisKindOfClassNSObjectisKindOfClass有所差别

我们可以通过GNUstep来查看NSProxy的源码实现,发现其内部会直接调用消息转发的方法,才会有我们将target替换成了ViewController对象,所以最后调用isKindOfClass的是ViewController对象,那么结果也就知晓了

从该方法可以反观NSProxy的其他方法内部实现,都会主动触发消息转发的实现

4. GCD定时器

GCD定时器相比其他两个定时器是最准时的,因为和系统内核直接挂钩

使用代码如下

我们将GCD定时器封装到自定义的LLTimer文件来使用

// HPTimer.h文件
@interface HPTimer : NSObject

+ (NSString *)execTask:(void(^)(void))task
           start:(NSTimeInterval)start
        interval:(NSTimeInterval)interval
         repeats:(BOOL)repeats
           async:(BOOL)async;

+ (NSString *)execTask:(id)target
              selector:(SEL)selector
                 start:(NSTimeInterval)start
              interval:(NSTimeInterval)interval
               repeats:(BOOL)repeats
                 async:(BOOL)async;

+ (void)cancelTask:(NSString *)name;


// HPTimer.m文件
#import "HPTimer.h"

@implementation HPTimer

static NSMutableDictionary *timers_;
static dispatch_semaphore_t semaphore_;

+ (void)initialize
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        timers_ = [NSMutableDictionary dictionary];
        
        // 加锁来保证多线程创建定时器和取消定时器同时只能有一个操作
        semaphore_ = dispatch_semaphore_create(1);
    });
}

+ (NSString *)execTask:(void (^)(void))task start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
{
    if (!task || start < 0 || (interval <= 0 && repeats)) return nil;
    
    // 队列
    dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();
    
    // 创建定时器
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
    // 设置时间
    // dispatch_time_t start:几秒后开始执行
    // uint64_t interval:执行间隔
    dispatch_source_set_timer(timer,
                              dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
                              interval * NSEC_PER_SEC, 0);
    
    
    dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
    // 定时器的唯一标识
    NSString *name = [NSString stringWithFormat:@"%zd", timers_.count];
    // 存放到字典中
    timers_[name] = timer;
    dispatch_semaphore_signal(semaphore_);
    
    // 设置回调
    dispatch_source_set_event_handler(timer, ^{
        task();
        
        if (!repeats) { // 不重复的任务
            [self cancelTask:name];
        }
    });
    
    // 启动定时器
    dispatch_resume(timer);
    
    return name;
}

+ (NSString *)execTask:(id)target selector:(SEL)selector start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
{
    if (!target || !selector) return nil;
    
    return [self execTask:^{
        if ([target respondsToSelector:selector]) {
            
// 去掉警告
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            [target performSelector:selector];
#pragma clang diagnostic pop
        }
    } start:start interval:interval repeats:repeats async:async];
}

+ (void)cancelTask:(NSString *)name
{
    if (name.length == 0) return;
    
    dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
    
    dispatch_source_t timer = timers_[name];
    if (timer) {
        dispatch_source_cancel(timer);
        [timers_ removeObjectForKey:name];
    }

    dispatch_semaphore_signal(semaphore_);
}

@end

然后在控制器里调用

#import "ViewController.h"
#import "HPTimer.h"

@interface ViewController ()

@property (copy, nonatomic) NSString *task;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"begin");
    
    // selector方式
    self.task = [HPTimer execTask:self
                         selector:@selector(doTask)
                            start:2.0
                         interval:1.0
                          repeats:YES
                            async:YES];
    
    // block方式
//    self.task = [HPTimer execTask:^{
//        NSLog(@"111111 - %@", [NSThread currentThread]);
//    } start:2.0 interval:-10 repeats:NO async:NO];
}

- (void)doTask
{
    NSLog(@"doTask - %@", [NSThread currentThread]);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [HPTimer cancelTask:self.task];
}

4.1 HPTimer

  • initialize方法里只执行一次字典的创建和锁的创建(只有用到该类时才创建,并且避免多次调用)
  • 内部创建一个全局的字典用来保存多个定时器的创建(定时器的个数递增作为key,timer为value)
  • 外部支持多个参数来控制定时器在哪个线程创建,以及是否只调用一次
  • 注意细节的优化,对于传入的时间、是否有任务,以及定时器的标识都对应做校验
  • 在多线程环境下,保证创建定时器和取消删除定时器同一时间只能有一个线程在执行

四、 面试题

从网上搜罗了一些面试题,我们不妨通过一些面试题来检查一下对知识点的掌握

1.看下面两段代码,会不会造成死锁

// 段1
- (void)viewDidLoad {
    [super viewDidLoad];
    
	NSLog(@"执行任务1");
	    
	dispatch_queue_t queue = dispatch_get_main_queue();
	dispatch_sync(queue, ^{
	   NSLog(@"执行任务2");
	});
	    
	NSLog(@"执行任务3");
}

// 段2
- (void)viewDidLoad {
    [super viewDidLoad];
    
	NSLog(@"执行任务1");
	    
	dispatch_queue_t queue = dispatch_get_main_queue();
	dispatch_async(queue, ^{
	   NSLog(@"执行任务2");
	});
	    
	NSLog(@"执行任务3");
}

第一段会死锁,第二段不会

因为整个函数viewDidLoad的执行都是在主队列中串行执行的,所以要等函数执行完才会执行任务2,但是dispatch_sync又是同步的,在主线程中是要执行dispatch_sync之后才会执行任务3的代码,所以互相之前都要等待,就造成了死锁

dispatch_async不会,因为需要等待一会才会执行任务2的代码,所以会先执行任务再执行任务2,不需要马上执行;但是不会开启新的线程

2.看下面这段代码,会不会造成死锁?将队列改为并发队列,会不会死锁

NSLog(@"执行任务1");
    
dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
    
dispatch_async(queue, ^{ // block0
   NSLog(@"执行任务2");
   
   dispatch_sync(queue, ^{ // block1
       NSLog(@"执行任务3");
   });
   
   NSLog(@"执行任务4");
});
    
NSLog(@"执行任务5");

会的。 原因是由于是dispatch_async,所以会先执行任务1和任务5,然后由于是串行队列,那么先会执行block0,再执行block1;但是任务2执行完,后面的是dispatch_sync,就表示要马上执行任务3,可任务3的执行又是要等block0执行完才可以,于是就会造成死锁

改为并发队列后不会死锁,虽然都是同一个并发队列,但是可以同时执行多个任务,不需要等待

3.看下面这段代码,会不会造成死锁?将队列2改为并发队列,会不会死锁

NSLog(@"执行任务1");
    
dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("myqueu2", DISPATCH_QUEUE_SERIAL);
    
dispatch_async(queue, ^{ // block0
   NSLog(@"执行任务2");
   
   dispatch_sync(queue2, ^{ // block1
       NSLog(@"执行任务3");
   });
   
   NSLog(@"执行任务4");
});
    
NSLog(@"执行任务5");

都不会。因为两个任务都是在两个队列里,所以不会有等待情况

4.看下面代码打印结果是什么,为什么,怎么改

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
dispatch_async(queue, ^{
   NSLog(@"1");
   [self performSelector:@selector(test) withObject:nil afterDelay:.0];
   NSLog(@"3");
});

- (void)test
{
    NSLog(@"2");
}

打印1、3。

因为performSelector: withObject: afterDelay:这个方法是属于RunLoop的库的,有afterDelay: 参数的本质都是往RunLoop中添加定时器的,由于当前是在子线程中,不会创建RunLoop,所以创建RunLoop后就可以执行该调用,并打印1、3、2

由于该方法的实现RunLoop是没有开源的,我们要想了解方法实现的本质,可以通过GNUstep开源项目来查看,这个项目将Cocoa的OC库重新开源实现了一遍,虽然不是官网源码,但也有一定的参考价值

源码地址:www.gnustep.org/resources/d…

我们在官网上找到GNUstep Base进行下载

然后找到RunLoop.mperformSelector: withObject: afterDelay:的实现

- (void) performSelector: (SEL)aSelector
	      withObject: (id)argument
	      afterDelay: (NSTimeInterval)seconds {
	      
  NSRunLoop	 *loop = [NSRunLoop currentRunLoop];
  GSTimedPerformer *item;

  item = [[GSTimedPerformer alloc] initWithSelector: aSelector
					     target: self
					   argument: argument
					      delay: seconds];
  [[loop _timedPerformers] addObject: item];
  RELEASE(item);
  [loop addTimer: item->timer forMode: NSDefaultRunLoopMode];
}

找到GSTimedPerformer的构造方法里面可以看到,会创建一个timer的定时器,然后将它加到RunLoop

- (id) initWithSelector: (SEL)aSelector
		 target: (id)aTarget
	       argument: (id)anArgument
		  delay: (NSTimeInterval)delay
{
  self = [super init];
  if (self != nil)
    {
      selector = aSelector;
      target = RETAIN(aTarget);
      argument = RETAIN(anArgument);
      timer = [[NSTimer allocWithZone: NSDefaultMallocZone()]
	initWithFireDate: nil
		interval: delay
		  target: self
		selector: @selector(fire)
		userInfo: nil
		 repeats: NO];
    }
  return self;
}

如此一来就印证了我们的分析,下面我们就手动在子线程创建RunLoop来查看

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
dispatch_async(queue, ^{
   NSLog(@"1");
   // 这句代码的本质是往Runloop中添加定时器
   [self performSelector:@selector(test) withObject:nil afterDelay:.0];
   NSLog(@"3");
   
   [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
});

- (void)test
{
    NSLog(@"2");
}

创建空的RunLoop之前因为已经通过performSelector: withObject: afterDelay:创建了一个定时器加了进去,所以RunLoop就不为空了,不需要我们再添加一个Source1了,这样也保证RunLoop不会退出

运行程序,打印结果为1、3、2

最后打印2是因为RunLoop被唤醒处理事件有时间延迟,所以会晚一些打印

5.看下面代码打印结果是什么,为什么,怎么改

- (void)test
{
    NSLog(@"2");
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
       NSLog(@"1");
    }];
  
    [thread start];
        
    [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}

打印结果为1,并且崩溃了。

因为执行线程的blockperformSelector几乎是同时的,所以先执行了block后的线程就被销毁了,这时再在该线程上发消息就是会报错

解决办法也是创建RunLoop并让子线程不被销毁

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"1");
        
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }];
    [thread start];
    
    [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}

6.dispatch_once是怎么做到只创建一次的,内部是怎么实现的

我们知道GCD中的dispatch_once的使用如下代码,可以做的只执行一次,一般用来创建单例

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    NSLog(@"单例应用");
});

我们可以通过源码来分析内部实现,在once.c中找到dispatch_once的实现

void
dispatch_once(dispatch_once_t *val, dispatch_block_t block)
{
	// val是onceToken静态变量
	dispatch_once_f(val, block, _dispatch_Block_invoke(block));
}

其中的_dispatch_Block_invoke是一个宏定义,用来包装block

#define _dispatch_Block_invoke(bb) \
		((dispatch_function_t)((struct Block_layout *)bb)->invoke)

找到其底层是通过dispatch_once_f实现的

void
dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func)
{
	// 将外界传入的静态变量val转变为dispatch_once_gate_t类型的变量l
	dispatch_once_gate_t l = (dispatch_once_gate_t)val;

#if !DISPATCH_ONCE_INLINE_FASTPATH || DISPATCH_ONCE_USE_QUIESCENT_COUNTER
	// 获取任务标识符v
	uintptr_t v = os_atomic_load(&l->dgo_once, acquire);
	// 如果v == DLOCK_ONCE_DONE,表示任务已经执行过了,直接return
	if (likely(v == DLOCK_ONCE_DONE)) {
		return;
	}
#if DISPATCH_ONCE_USE_QUIESCENT_COUNTER
	// 如果加锁失败走到这里,再次进行存储,并标记为DLOCK_ONCE_DONE
	if (likely(DISPATCH_ONCE_IS_GEN(v))) {
		return _dispatch_once_mark_done_if_quiesced(l, v);
	}
#endif
#endif
	if (_dispatch_once_gate_tryenter(l)) { // 尝试进入任务
		return _dispatch_once_callout(l, ctxt, func);
	}
	
	// 此时已有任务,则进入无限等待
	return _dispatch_once_wait(l);
}

dispatch_once_f函数的详细调用分析

1.os_atomic_load这个宏用来获取任务标识

#define os_atomic_load(p, m) \
		atomic_load_explicit(_os_atomic_c11_atomic(p), memory_order_##m)

2.通过_dispatch_once_mark_done_if_quiesced进行再次存储和标记

DISPATCH_ALWAYS_INLINE
static inline void
_dispatch_once_mark_done_if_quiesced(dispatch_once_gate_t dgo, uintptr_t gen)
{
	if (_dispatch_once_generation() - gen >= DISPATCH_ONCE_GEN_SAFE_DELTA) {
		/*
		 * See explanation above, when the quiescing counter approach is taken
		 * then this store needs only to be relaxed as it is used as a witness
		 * that the required barriers have happened.
		 */
		
		// 再次存储,并标记为DLOCK_ONCE_DONE
		os_atomic_store(&dgo->dgo_once, DLOCK_ONCE_DONE, relaxed);
	}
}

3.通过_dispatch_once_gate_tryenter内部进行比较并加锁

DISPATCH_ALWAYS_INLINE
static inline bool
_dispatch_once_gate_tryenter(dispatch_once_gate_t l)
{
	// 进行比较,如果没问题,则进行加锁,并标记为DLOCK_ONCE_UNLOCKED
	return os_atomic_cmpxchg(&l->dgo_once, DLOCK_ONCE_UNLOCKED,
			(uintptr_t)_dispatch_lock_value_for_self(), relaxed);
}

4.通过_dispatch_once_callout来执行回调

DISPATCH_NOINLINE
static void
_dispatch_once_callout(dispatch_once_gate_t l, void *ctxt,
		dispatch_function_t func)
{
	// 回调执行
	_dispatch_client_callout(ctxt, func);
	// 进行广播
	_dispatch_once_gate_broadcast(l);
}

_dispatch_client_callout内部就是执行block回调

DISPATCH_ALWAYS_INLINE
static inline void
_dispatch_client_callout(void *ctxt, dispatch_function_t f)
{
	return f(ctxt);
}

_dispatch_once_gate_broadcast内部会调用_dispatch_once_mark_done

DISPATCH_ALWAYS_INLINE
static inline void
_dispatch_once_gate_broadcast(dispatch_once_gate_t l)
{
	dispatch_lock value_self = _dispatch_lock_value_for_self();
	uintptr_t v;
#if DISPATCH_ONCE_USE_QUIESCENT_COUNTER
	v = _dispatch_once_mark_quiescing(l);
#else
	v = _dispatch_once_mark_done(l);
#endif
	if (likely((dispatch_lock)v == value_self)) return;
	_dispatch_gate_broadcast_slow(&l->dgo_gate, (dispatch_lock)v);
}

_dispatch_once_mark_done内部就是赋值并标记,即解锁

DISPATCH_ALWAYS_INLINE
static inline uintptr_t
_dispatch_once_mark_done(dispatch_once_gate_t dgo)
{
	// 如果不相同,则改成相同,并标记为DLOCK_ONCE_DONE
	return os_atomic_xchg(&dgo->dgo_once, DLOCK_ONCE_DONE, release);
}

五、总结:

GCD单例中,有两个重要参数,onceTokenblock,其中onceToken是静态变量,具有唯一性,在底层被封装成了dispatch_once_gate_t类型的变量ll主要是用来获取底层原子封装性的关联,即变量v,通过v来查询任务的状态,如果此时v等于DLOCK_ONCE_DONE,说明任务已经处理过一次了,直接return

如果此时任务没有执行过,则会在底层通过C++函数的比较,将任务进行加锁,即任务状态置为DLOCK_ONCE_UNLOCK,目的是为了保证当前任务执行的唯一性,防止在其他地方有多次定义。加锁之后进行block回调函数的执行,执行完成后,将当前任务解锁,将当前的任务状态置为DLOCK_ONCE_DONE,在下次进来时,就不会在执行,会直接返回

猜你喜欢

转载自juejin.im/post/7116907029465137165