22-Explore the underlying principles of iOS | Multithreading technology [atomic lock atomic, gcd Timer, NSTimer, CADisplayLink]

foreword

Before, when we were exploring the principles of animation and rendering, we output several articles and answered them iOS动画是如何渲染,特效是如何工作的疑惑. We deeply feel that system designers are so open-minded when creating these system frameworks, and also深深意识到了解一门技术的底层原理对于从事该方面工作的重要性。

So we decided 进一步探究iOS底层原理的任务. Following the previous article to understand the exploration of [10 thread locks in iOS, and thread lock types: spin locks, mutex locks, recursive locks], this article will continue to explore the underlying principles of GCD multithreading

1. Atomic lock

1. atomic

atomicsetter、getterThe atomic operation used to ensure the attribute is equivalent to getter和setteradding a thread synchronization lock internally.

Atomicity: An atom is the smallest physical unit, which means that it cannot be divided; that is, the code is operated as a whole in the same thread

atomicIt is only guaranteed to setter、getterbe thread-safe, it does not guarantee that the process of using the property is thread-safe

2. From the source code analysis getter和setterfor atomicthe use of

We find the corresponding objc4implementation inobjc-accessors.mmgetter和setter

getterrealization

// 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);
}

setterrealization

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);
}

It can be seen from the source code that only automicthe attributes will be locked.

Second, the choice of thread locks in the read-write security scheme in iOS

Consider the following scenarios, how to do the most appropriate

  • At the same time, only one thread can perform the write operation
  • At the same time, multiple threads are allowed to read
  • At the same time, both write and read operations are not allowed

The above scenario is a typical "multiple read and single write", which is often used for reading and writing data such as files. The implementation schemes in iOS include the following two

  • pthread_rwlock: Read-write lock
  • dispatch_barrier_async: asynchronous fence call

1. pthread_rwlock

pthread_rwlockIt is a lock dedicated to reading and writing files, and its essence is also a mutual exclusion lock. The thread waiting for the lock will go to sleep

Use the code as follows

@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_asyncAlso called the fence function, it is intended to intercept multi-threaded asynchronous concurrent operations, only to ensure that there is one thread operating at the same time

Using the fence function can also guarantee the operation of multiple reads and single writes

Use the code as follows

@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

3. Timer

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

  • 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,在下次进来时,就不会在执行,会直接返回

Guess you like

Origin juejin.im/post/7116907029465137165