玩转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]);
}

控制台输出如下:


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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容

  • 一:base.h 二:block.h 1. dispatch_block_flags:DISPATCH_BLOCK...
    小暖风阅读 2,404评论 0 0
  • 在开发中使用单例是最经常不过的事儿了,最常用的就是dispatch_once这个函数,这个函数可以使其参数内的bl...
    ilovehusky阅读 13,174评论 8 52
  • 发现写博客想写明白也是一件不容易的事情。 这次拿YYKIt 源码 分析分析。希望这次能写的更好些。 YYKit 系...
    充满活力的早晨阅读 6,539评论 4 16
  • 目录 dispatch_once dispatch_once低负载特性 备注 参考文章 相信大家对dispatch...
    时间在改变阅读 1,548评论 0 6
  • 同步/异步 同步:多个任务情况下,一个任务A执行结束,才可以执行另一个任务B。只存在一个线程也就是主线程。 异步:...
    XLsn0w阅读 296评论 0 0