玩转dispatch_once

前言

说起dispatch_once,最先想到的可能是单例,比如常用的AFNetworking中是这么写的:

+ (instancetype)sharedManager {
    static AFNetworkReachabilityManager *_sharedManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _sharedManager = [self manager];
    });

    return _sharedManager;
}

但是为什么这样写就可以确保dispatch_once中的block只执行一次?dispatch_once的原理是怎么样的?有没有可能让dispatch_once中的block执行多次?

基本认知

  • dispatch_once_t
typedef long dispatch_once_t;

dispatch_once_t其实是long类型

  • DISPATCH_NOESCAPE
#if __has_attribute(noescape)
#define DISPATCH_NOESCAPE __attribute__((__noescape__))
#else
#define DISPATCH_NOESCAPE
#endif

void dispatch_once(dispatch_once_t *predicate, DISPATCH_NOESCAPE dispatch_block_t block);

DISPATCH_NOESCAPE多用来修饰block,用于表明block在当前方法执行结束前执行。类似于Swift中的@noescape(非逃逸闭包),与之对应的是@escaping(逃逸闭包)。简单地说,闭包在函数结束前被调用的为非逃逸闭包,闭包在函数结束后被调用的为逃逸闭包。

  • dispatch_block_t
typedef void (^dispatch_block_t)(void);

返回值是void,参数是void的block

  • dispatch_function_t
typedef void (*dispatch_function_t)(void *_Nullable);

返回值是void,参数是void *的函数指针

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

函数指针,指向结构体Block_layout中的invoke。此处涉及到block结构体,本文不做深入探究,须知invoke为block具体实现即可。

  • dispatch_once_gate_t
typedef struct dispatch_once_gate_s {
    union {
        dispatch_gate_s dgo_gate;
        uintptr_t dgo_once;
    };
} dispatch_once_gate_s, *dispatch_once_gate_t;

typedef struct dispatch_gate_s {
    dispatch_lock dgl_lock;
} dispatch_gate_s, *dispatch_gate_t;

typedef uint32_t dispatch_lock;

dispatch_once_gate_t为指向dispatch_once_gate_s的结构体指针

  • DISPATCH_DECL
    刚提到dispatch_once_gate_t和dispatch_once_gate_s,顺便说说与之相关的DISPATCH_DECL
#define DISPATCH_DECL(name) typedef struct name##_s *name##_t

如果这样写,DISPATCH_DECL(dispatch_once_gate),展开后变成

typedef struct dispatch_once_gate_s *dispatch_once_gate_t

这其实就是上文中的声明了,用来确保编译可以通过。
源码中存在很多类似声明,如:

DISPATCH_DECL(dispatch_group);
DISPATCH_DECL(dispatch_queue);
  • DLOCK_ONCE_DONE
#define DLOCK_ONCE_DONE     (~(uintptr_t)0)
typedef unsigned long       uintptr_t;

对0按位取反,带入后DLOCK_ONCE_DONE的值为-1(计算机基础知识:源码、反码、补码)

  • DLOCK_ONCE_UNLOCKED
#define DLOCK_ONCE_UNLOCKED ((uintptr_t)0)

DLOCK_ONCE_UNLOCKED的值为0

简单解析

void dispatch_once(dispatch_once_t *val, dispatch_block_t block) {
    dispatch_once_f(val, block, _dispatch_Block_invoke(block));
}

void dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func) {
    dispatch_once_gate_t l = (dispatch_once_gate_t)val;

#if !DISPATCH_ONCE_INLINE_FASTPATH || DISPATCH_ONCE_USE_QUIESCENT_COUNTER
    uintptr_t v = os_atomic_load(&l->dgo_once, acquire);
    if (likely(v == DLOCK_ONCE_DONE)) {
        return;
    }
#if DISPATCH_ONCE_USE_QUIESCENT_COUNTER
    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);
}

可以看到,流程很简单。

  • v == DLOCK_ONCE_DONE
    v == DLOCK_ONCE_DONE时,直接return。此时对应着单例已经初始化完成,所以不会执行block

  • _dispatch_once_gate_tryenter

DISPATCH_ALWAYS_INLINE
static inline bool
_dispatch_once_gate_tryenter(dispatch_once_gate_t l)
{
    return os_atomic_cmpxchg(&l->dgo_once, DLOCK_ONCE_UNLOCKED,
            (uintptr_t)_dispatch_lock_value_for_self(), relaxed);
}

这是一个返回值为bool类型的内联函数,当返回值为true时,对应执行block。

  • _dispatch_once_wait
    此时当前block正在执行,对应场景如,多线程访问单例,线程1访问时,block未执行过,此时执行block。同时,线程2也在访问单例,由于线程1block未执行完毕,所以走_dispatch_once_wait逻辑等待,直到线程1block执行完毕(此处更严谨地说,是_dispatch_once_gate_broadcast未执行完,而非block未执行完,为方便理解,这里直接说block,关于_dispatch_once_gate_broadcast下文会有介绍)

细节分析

  • v == DLOCK_ONCE_DONE

问题:为什么v和DLOCK_ONCE_DONE比较可以判断block是否执行过?
先把这个问题简化为如下代码(因为这些类型未公开,此处重写用来模拟数据结构):

typedef uint32_t jk_dispatch_lock;

typedef struct jk_dispatch_gate_s {
    jk_dispatch_lock dgl_lock;
} jk_dispatch_gate_s, *jk_dispatch_gate_t;

typedef struct jk_dispatch_once_gate_s {
    union {
        jk_dispatch_gate_s dgo_gate;
        uintptr_t dgo_once;
    };
} jk_dispatch_once_gate_s, *jk_dispatch_once_gate_t;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    dispatch_once_t token = 1;
    dispatch_once_t *val = &token;
    jk_dispatch_once_gate_t l = (jk_dispatch_once_gate_t)val;
    NSLog(@"%ld", l->dgo_once);
}

可以发现,l->dgo_once始终等于token。因为val指向token的地址,所以l指向token的地址,l->dgo_once取到的值就是token。那么,gcd源码中的l->dgo_once是何时被赋值的?这就说到第二个条件_dispatch_once_gate_tryenter了

  • _dispatch_once_gate_tryenter
DISPATCH_ALWAYS_INLINE
static inline bool
_dispatch_once_gate_tryenter(dispatch_once_gate_t l)
{
    return os_atomic_cmpxchg(&l->dgo_once, DLOCK_ONCE_UNLOCKED,
            (uintptr_t)_dispatch_lock_value_for_self(), relaxed);
}

#define os_atomic_cmpxchg(p, e, v, m) \
        ({ _os_atomic_basetypeof(p) _r = (e); \
        atomic_compare_exchange_strong_explicit(_os_atomic_c11_atomic(p), \
        &_r, v, memory_order_##m, memory_order_relaxed); })

可以看到,最终调用atomic_compare_exchange_strong_explicit,简单介绍下这个函数(原子操作):
1.l->dgo_once与DLOCK_ONCE_UNLOCKED相等,那么将_dispatch_lock_value_for_self()赋值给l->dgo_once,并返回true;

  1. l->dgo_once与DLOCK_ONCE_UNLOCKED不等,那么将DLOCK_ONCE_UNLOCKED赋值给l->dgo_once,并返回false

通常单例这么写:

+ (instancetype)shared {
    static xx *one;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        one = [[self alloc] init];
    });
    return one;
}

此时onceToken的值默认为0,所以首次执行shared方法时,调用_dispatch_once_gate_tryenter返回值为true(上文说过DLOCK_ONCE_UNLOCKED的值为0),此时onceToken被赋值为_dispatch_lock_value_for_self()。但是_dispatch_lock_value_for_self()并不等于DLOCK_ONCE_DONE,那么如何确保block只执行一次?原因在下一个函数_dispatch_once_callout中

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

首次调用shared方法_dispatch_once_gate_tryenter会将onceToken的值设为_dispatch_lock_value_for_self()并返回true,所以会调用_dispatch_once_callout,_dispatch_client_callout是用来调用block的(如何调用下文会解析),而_dispatch_once_gate_broadcast会改变onceToken的值

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_ALWAYS_INLINE
static inline uintptr_t
_dispatch_once_mark_done(dispatch_once_gate_t dgo)
{
    return os_atomic_xchg(&dgo->dgo_once, DLOCK_ONCE_DONE, release);
}

#define os_atomic_xchg(p, v, m) \
        atomic_exchange_explicit(_os_atomic_c11_atomic(p), v, memory_order_##m)

可见,会调用_dispatch_once_mark_done赋值给v,然后比较v与_dispatch_lock_value_for_self()的值。_dispatch_once_mark_done内部调用os_atomic_xchg,简单介绍这个函数(原子操作):
将DLOCK_ONCE_DONE赋值给dgo->dgo_once,并返回dgo->dgo_once原值(被赋值前的值)
所以此时l->dgo_once值是DLOCK_ONCE_DONE,即onceToken值为DLOCK_ONCE_DONE(-1)。而v的值是_dispatch_lock_value_for_self(),所以此时v等于value_self,_dispatch_once_gate_broadcast函数return。

当shared方法再次被调用时,因为onceToken值为DLOCK_ONCE_DONE,所以直接return,所以block不会再次执行。

  • _dispatch_client_callout
    现在回过头来说说block是怎样被执行的
void _dispatch_client_callout(void *ctxt, dispatch_function_t f) {
    @try {
        return f(ctxt);
    }
    @catch (...) {
        objc_terminate();
    }
}

好像不太好懂,将代码简化为如下所示:

typedef void(*JK_BlockInvokeFunction)(void *, ...);

struct JK_Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved;
    JK_BlockInvokeFunction invoke;
//    struct Block_descriptor_1 *descriptor;
        // imported variables
};

- (void)viewDidLoad {
    [super viewDidLoad];

    dispatch_block_t block = ^{
        NSLog(@"执行");
    };
    
    void *ctxt = (__bridge void *)(block);
    struct JK_Block_layout *layout = (__bridge struct JK_Block_layout *)block;
    dispatch_function_t func = (dispatch_function_t)(layout->invoke);
    func(ctxt);
}

控制台会输出执行字样,上文说过,layout->invoke为block的具体实现,func是函数指针,那么如何调用这个函数,显然后面加()就会执行,但是这个block并没有参数,为什么要在func中传入ctxt?如果不了解,可以阅读我之前写过的一篇文章:强大的NSInvocation

其实block有一个隐藏参数target,而这个target就是block本身,所以执行func(ctxt)相当于执行block()

  • _dispatch_once_wait
    上文说过,当多线程访问时,可能会执行_dispatch_once_wait,感兴趣可以看一下源码,没有太多疑难点,这里不做解析

验证onceToken控制变量的正确性

@implementation Test
+ (instancetype)shared {
    static Test *t;
    static dispatch_once_t onceToken;
    NSLog(@"before: %ld", onceToken);
    dispatch_once(&onceToken, ^{
        t = [[Test alloc] init];
        NSLog(@"middle: %ld", onceToken);
    });
    NSLog(@"after: %ld", onceToken);
    return t;
}
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"%@", [Test shared]);
    NSLog(@"%@", [Test shared]);
    NSLog(@"%@", [Test shared]);
}

控制台输出如下:


329694-30d51f341d740b8a.png
屏幕快照 2019-05-24 18.16.13.png

和之前的源码解读完全吻合

如果将初始值设为-1会怎样?

+ (instancetype)shared {
    static Test *t;
    static dispatch_once_t onceToken = -1;
    NSLog(@"before: %ld", onceToken);
    dispatch_once(&onceToken, ^{
        t = [[Test alloc] init];
        NSLog(@"middle: %ld", onceToken);
    });
    NSLog(@"after: %ld", onceToken);
    return t;
}
329694-efab9dd233a70a3e.png
屏幕快照 2019-05-24 18.27.22.png

可以看到,block不会执行,所以返回值为空,同样符合上文分析。

如果设置一个既不为0也不为-1的值会怎样?比如设置为1,会发现程序crash,并定位在如下图所示:


329694-71f9983e84c513a9.png
屏幕快照 2019-05-24 18.30.30.png

再次分析:首次执行时调用_dispatch_once_gate_tryenter,由于onceToken初始值为1,所以返回false并将0赋值给onceToken,返回false导致无法执行_dispatch_once_callout,所以block不会执行,onceToken也不会被赋值为-1。而是直接执行_dispatch_once_wait,从而导致crash

如何让block执行多次

前面说了这么多,现在来玩点小花招,让dispatch_once的block执行多次(这样单例就失效了)

通过上文分析不难发现,block是否执行其实是通过onceToken的值来控制的,所以从这里下手,代码这样写:

static dispatch_once_t onceToken;
@implementation Test
+ (instancetype)shared {
    static Test *t;
//    static dispatch_once_t onceToken;
    NSLog(@"before: %ld", onceToken);
    dispatch_once(&onceToken, ^{
        t = [[Test alloc] init];
        NSLog(@"middle: %ld", onceToken);
    });
    NSLog(@"after: %ld", onceToken);
    return t;
}
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"%@--%ld", [Test shared], onceToken);
    onceToken = 0;
    NSLog(@"%@--%ld", [Test shared], onceToken);
    onceToken = 0;
    NSLog(@"%@--%ld", [Test shared], onceToken);
}

控制台输出如下:


329694-016ba2b30216669b.png
屏幕快照 2019-05-24 18.20.24.png

可以看到,block被执行了3次,并且每次返回的Test实例都不一样


2019.05.29更新:
偶然看到网上有人写dispatch_once混合调用导致死锁的问题,文章分析了一通感觉没说到点子上,这里简单分析下原因:

@interface TestA : NSObject
+ (instancetype)shared;
@end

@interface TestB : NSObject
+ (instancetype)shared;
@end

@implementation TestA
+ (instancetype)shared {
    static TestA *a;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        a = [[self alloc] init];
    });
    return a;
}

- (instancetype)init {
    if (self = [super init]) {
        [TestB shared];
    }
    return self;
}
@end

@implementation TestB
+ (instancetype)shared {
    static TestB *b;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        b = [[self alloc] init];
    });
    return b;
}

- (instancetype)init {
    if (self = [super init]) {
        [TestA shared];
    }
    return self;
}
@end


- (void)viewDidLoad {
    [super viewDidLoad];
    
    [TestA shared];
}

可以看到,调用TestA的shared方法时,A的内部会调用TestB的shared方法,而B的内部又调用了A。上文分析过,首次调用时,会把onceToken置为_dispatch_lock_value_for_self(),此操作在执行block之前,当执行block时,block内部调用B而B内部又调用了A,此时A的onceToken是一个既不为0也不为-1的值,所以走wait逻辑,从而导致B的block无法执行完毕,而B的block无法执行完毕导致A的block无法执行完毕,此处形成相互等待,从而导致crash


Have fun!

猜你喜欢

转载自blog.csdn.net/weixin_33749242/article/details/90771279