GCD 捕獲 self 是否會形成內存泄漏?

背景

關於 GCD 的 block 捕獲 self 是否形成循環引用的問題,網上是爭論不休,在 iOS 的面試中更是頻繁出現。咱們從 YYKit 裏面的一個 Issue 出發,來探索一下 GCD 跟 self 之間是否會形成循環引用的問題。git

該 Issue 起源於 YYKit 中的一段代碼:github

- (void)_trimInBackground {
    __weak typeof(self) _self = self;
    dispatch_async(_queue, ^{
        __strong typeof(_self) self = _self;
        /* 此處省略一萬字 **/
    });
}
複製代碼

能夠看到,YY 大神在 GCD 中,爲了不循環引用,使用了 strong-weak dance,可是網友在該 Issue 中提出,蘋果的 dispatch_async 函數在 block 任務執行完成後會將該 block 進行 Block_release,並不會形成循環引用,此處用 strong-weak dance 反而可能形成 block 執行前 self 就已經被釋放。面試

而 YY 大神的觀點則認爲,因爲self 持有一個 _queue 變量,而 _queue 會持有該 block,此時在 block 內直接捕獲 self 則會形成循環引用。(self->_queue->block->self)api

然而這樣真的會形成內存泄漏嗎?async

探索

咱們建立一個簡單的 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 的頁面,而後關掉;若是 dispatch_async 強捕獲 self 會形成內存泄漏,那麼 ViewController2 的 dealloc 方法必然是不會執行的。 執行結果以下:源碼分析

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 被正常釋放了,也就是說並不會形成內存泄漏ui

源碼分析

源碼面前無祕密,咱們看一下 dispatch_async 的源碼:this

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(也就是咱們傳進來的任務 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 最終會與其餘參數封裝成 dispatch_continuation_t,咱們重點看一下這塊封裝的代碼(如下代碼有所精簡):atom

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) {
        // 顧名思義,執行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();	// 執行
	Block_release(b);	// 釋放
}
複製代碼

能夠看到正如 Apple 文檔所說,dispatch_async 會在 block 執行完成後將其釋放。所以 _self->queue->block->self 這個循環引用只是暫時的(block 執行完成後被釋放,打斷了循環引用)。

刨根問底

dispatch_sync

既然 dispatch_async 的 block 捕獲 self 不會形成循環引用,那麼換成 dispatch_sync 會怎麼樣呢?

self.queue = dispatch_queue_create("com.mayc.concurrent", DISPATCH_QUEUE_CONCURRENT);
 dispatch_sync(self.queue, ^{
     [self test];
 });
複製代碼

其實 dispatch_sync 也不會有問題。咱們把剛剛 Demo 中的 dispatch_async 換成 dispatch_sync ,能夠看到也未形成內存泄漏。

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 循環引用的緣由跟 dispatch_async 有所不一樣,不是由於執行完成後被 release,咱們看一下官方關於 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 進行持有,也不會進行 Block_copy 操做。既然 queue -> block 這一層引用不存在,天然也不會形成循環引用

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 處理,所以其餘的這些也不會由於捕獲 self 而形成循環引用。

拓展

既然就算 self 持有 queue 也不會形成 GCD 的循環引用,那若是是 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...若是非要這樣的話,確定是會內存泄漏的....這是由於 block 被 self 直接持有,同時在 gcd 中進行了一次 Block_copy 操做,引用計數器爲 2。block 任務執行完成後進行 Block_release,此時引用計數器爲1 ,這種狀況下 block 不會被清理。

相關文章
相關標籤/搜索