前言
说起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;
- 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!
网友评论