關(guān)于 GCD 的 block 捕獲 self 是否造成循環(huán)引用的問題,網(wǎng)上是爭論不休痢毒,在 iOS 的面試中更是頻繁出現(xiàn)送矩。我們從 YYKit 里面的一個?Issue
出發(fā),來探索一下 GCD 跟 self 之間是否會造成循環(huán)引用的問題哪替。
該 Issue 起源于 YYKit 中的一段代碼:
- (void)_trimInBackground {
__weak typeof(self) _self = self;
dispatch_async(_queue, ^{
__strong typeof(_self) self = _self;
/* 此處省略一萬字 **/
});
}
可以看到栋荸,YY 大神在 GCD 中,為了避免循環(huán)引用凭舶,使用了 strong-weak dance晌块,但是網(wǎng)友在該 Issue 中提出,蘋果的 dispatch_async 函數(shù)在 block 任務(wù)執(zhí)行完成后會將該 block 進(jìn)行 Block_release帅霜,并不會造成循環(huán)引用匆背,此處用 strong-weak dance 反而可能造成 block 執(zhí)行前 self 就已經(jīng)被釋放。
而 YY 大神的觀點(diǎn)則認(rèn)為身冀,由于self 持有一個 _queue 變量靠汁,而 _queue 會持有該 block蜂大,此時在 block 內(nèi)直接捕獲 self 則會造成循環(huán)引用。(self->_queue->block->self)
然而這樣真的會造成內(nèi)存泄漏嗎蝶怔?
探索
我們創(chuàng)建一個簡單的 Demo,代碼如下:
@interface ViewController2 ()
@property (nonatomic, strong) dispatch_queue_t queue;
@end
@implementation ViewController2
- (void)viewDidLoad {
[super viewDidLoad];
self.queue = dispatch_queue_create("com.mayc.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(self.queue, ^{
[self test];
});
}
- (void)test {
NSLog(@"test");
}
- (void)dealloc {
NSLog(@"dealloc");
}
@end
在 Demo 里兄墅,ViewController2 持有一個 queue 變量踢星,dispatch_async 的 block 中捕獲了 self。我們打開一個?ViewController2 的頁面隙咸,然后關(guān)掉沐悦;如果?dispatch_async 強(qiáng)捕獲 self 會造成內(nèi)存泄漏,那么?ViewController2 的 dealloc 方法必然是不會執(zhí)行的五督。 執(zhí)行結(jié)果如下:
2020-03-11 15:36:35.352789+0800 MCDemo[83661:22062265] test
2020-03-11 15:36:36.922477+0800 MCDemo[83661:22062108] dealloc
可以看到?ViewController2 被正常釋放了藏否,也就是說?并不會造成內(nèi)存泄漏
。
源碼分析
源碼面前無秘密充包,我們看一下 dispatch_async 的源碼:
void dispatch_async(dispatch_queue_t dq, dispatch_block_t work) {
dispatch_continuation_t dc = _dispatch_continuation_alloc();
uintptr_t dc_flags = DC_FLAG_CONSUME;
dispatch_qos_t qos;
// 將work(也就是我們傳進(jìn)來的任務(wù) block)封裝成 dispatch_continuation_t
qos = _dispatch_continuation_init(dc, dq, work, 0, dc_flags);
_dispatch_continuation_async(dq, dc, qos, dc->dc_flags);
}
可以看到副签,dispatch_async 傳入的 block 最終會與其他參數(shù)封裝成?dispatch_continuation_t,我們重點(diǎn)看一下這塊封裝的代碼(以下代碼有所精簡):
static inline dispatch_qos_t
_dispatch_continuation_init(dispatch_continuation_t dc,
dispatch_queue_class_t dqu, dispatch_block_t work,
dispatch_block_flags_t flags, uintptr_t dc_flags)
{
// 拷貝block
void *ctxt = _dispatch_Block_copy(work);
dc_flags |= DC_FLAG_BLOCK | DC_FLAG_ALLOCATED;
dispatch_function_t func = _dispatch_Block_invoke(work);
if (dc_flags & DC_FLAG_CONSUME) {
// 顧名思義基矮,執(zhí)行block淆储,然后釋放。
func = _dispatch_call_block_and_release;
}
return _dispatch_continuation_init_f(dc, dqu, ctxt, func, flags, dc_flags);
}
// _dispatch_call_block_and_release的源碼如下
void _dispatch_call_block_and_release(void *block)
{
void (^b)(void) = block;
b(); // 執(zhí)行
Block_release(b); // 釋放
}
可以看到正如 Apple 文檔所說家浇,dispatch_async 會在 block 執(zhí)行完成后將其釋放本砰。因此?_self->?queue->block->self
這個循環(huán)引用只是暫時的(block 執(zhí)行完成后被釋放,打斷了循環(huán)引用)钢悲。
刨根問底
dispatch_sync
既然 dispatch_async 的 block 捕獲 self 不會造成循環(huán)引用点额,那么換成 dispatch_sync 會怎么樣呢?
self.queue = dispatch_queue_create("com.mayc.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(self.queue, ^{
[self test];
});
其實(shí) dispatch_sync 也不會有問題莺琳。我們把剛剛 Demo 中的 dispatch_async 換成 dispatch_sync 还棱,可以看到也未造成內(nèi)存泄漏。
2020-03-11 17:05:18.840834+0800 MCDemo[5437:69508] test
2020-03-11 17:05:20.419588+0800 MCDemo[5437:68626] dealloc
不過?dispatch_sync 不會造成?_self->?queue->block->self
循環(huán)引用的原因跟?dispatch_async 有所不同芦昔,不是因?yàn)閳?zhí)行完成后被 release诱贿,我們看一下官方關(guān)于?dispatch_sync 的文檔有段說明:
Unlike with?dispatch_async
, no retain is performed on the target queue. Because calls to this function are synchronous, it “borrows” the reference of the caller. Moreover, no?Block_copy
is performed on the block.
大致意思是說,queue 不會對 block 進(jìn)行持有咕缎,也不會進(jìn)行 Block_copy 操作珠十。既然 queue -> block 這一層引用不存在,自然也?不會造成循環(huán)引用
凭豪。
dispatch_after 等其他 GCD api
我們在 dispatch_after焙蹭、dispatch_group_async 的官方文檔里面也看可以到和 dispatch_async 類似的話:
_This function performs a?Block_copy
and?Block_release
on behalf of the caller.?_
可以看到這些 GCD api 的方法也都做了 release 處理,因此其他的這些也不會因?yàn)椴东@ self 而造成循環(huán)引用嫂伞。
拓展
既然就算 self 持有 queue 也不會造成 GCD 的循環(huán)引用孔厉,那如果是 self 直接持有 GCD 的 block 呢拯钻?
dispatch_queue_t queue = dispatch_queue_create("com.mayc.concurrent", DISPATCH_QUEUE_CONCURRENT);
self.block = ^{
[self test];
};
dispatch_async(queue, self.block);
emm…如果非要這樣的話,肯定是會內(nèi)存泄漏的….這是因?yàn)?block 被 self 直接持有撰豺,同時在 gcd 中進(jìn)行了一次 Block_copy 操作粪般,引用計數(shù)器為 2。block 任務(wù)執(zhí)行完成后進(jìn)行 Block_release污桦,此時引用計數(shù)器為1 亩歹,這種情況下 block 不會被清理。