玩转dispatch_once

作者: 01_Jack | 来源:发表于2019-05-24 18:22 被阅读113次

    前言

    说起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]);
    }
    

    控制台输出如下:


    屏幕快照 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;
    }
    
    屏幕快照 2019-05-24 18.27.22.png

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

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


    屏幕快照 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);
    }
    

    控制台输出如下:


    屏幕快照 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!

    相关文章

      网友评论

        本文标题:玩转dispatch_once

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