weak-strong dance 簡介
使用 Block 時可以通過__weak
來避免迴圈引用已經是眾所周知的事情:
1 2 3 4 |
// OCClass.m __weak typeof(self) weakSelf = self; self.handler = ^{ NSLog(@"Self is %@", weakSelf); }; |
這時handler
持有 Block 物件,而 Block 物件雖然捕獲了weakSelf
,延長了weakSelf
這個區域性變數的生命週期,但weakSelf
是附有__weak
修飾符的變數,它並不會持有物件,一旦它指向的物件被廢棄了,它將自動被賦值為nil
。在多執行緒情況下,可能weakSelf
指向的物件會在 Block 執行前被廢棄,這在上例中無傷大雅,只會輸出Self is nil
,但在有些情況下(譬如weakSelf
作為 KVO 的觀察者被移除時)就會導致 crash。這時可以在 Block 內部再持有一次weakSelf
指向的物件,延長該物件的生命週期,這就是所謂的 weak-strong dance:
1 2 3 4 5 6 |
__weak typeof(self) weakSelf = self; self.handler = ^{ typeof(weakSelf) strongSelf = weakSelf; [strongSelf.obserable removeObserver:strongSelf forKeyPath:kObservableProperty]; }; |
typeof(weakSelf) strongSelf = weakSelf
這一句等於__strong typeof(weakSelf) strongSelf = weakSelf
,在 ARC 模式下,id 型別和 OC 物件型別預設的所有權修飾符就是__strong
,所以是可以省略的。
問題
上面就是對 weak-strong dance 的掃盲級描述。不知道大家怎麼想,反正我剛聽說這個東西的時候,是有幾個疑惑的:
self
指向的物件已經被廢棄的情況下,_handler
成員變數也不存在了,在 ARC 下會自動釋放它指向的 Block 物件,這個時候 Block 物件應該已經沒有被變數所持有了,它的引用計數應該已經為0了,它應該被廢棄了啊,為什麼它還能繼續存在並執行。- 本來在 Block 內部使用
weakSelf
就是為了讓 Block 物件不持有self
指向的物件,那在 Block 內部又把weakSelf
賦給strongSelf
不就又持有self
物件了麼?又迴圈引用了?
要解決以上疑惑,需要對 ARC、Block、GCD 這些概念有比較深入的瞭解,主要是要清楚 Block 的實現原理。離職前不久我在公司做過一個關於函數語言程式設計的內部分享,講完 PPT 後有個同學問我“閉包”是怎麼實現的,我當時沒有細說,因為不同語言在實現同一個概念時肯定會有一些差異,我也不是什麼語言都精通,所以不敢妄議。現在我也不敢說對所有語言的“閉包”實現都瞭如指掌,但至少對 OC 的閉包實現——Block 還算心中有數的。下面先簡單介紹一下 Block 的實現,當然篇幅所限,會略過一些跟今天的主題關係不大的細節。
Block 的實現
Block 是 C 語言的擴充套件功能,支援 Block 的編譯器會把含有 Block 的程式碼轉換成一般的 C 程式碼執行。之前我一直有用到“Block 物件”這個詞,因為一個 Block 例項就是一個含有“isa”指標的結構體,跟一般的 OC 物件的結構是一樣的:
1 2 3 4 5 6 7 8 9 10 11 |
struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; struct __xx_block_impl_x { struct __block_impl impl; // ... }; |
所以跟一般的 OC 物件一樣,這個isa
指標也指向該 Block 例項的型別結構體(類物件,也有叫單件類的),Block 有三種型別:
- _NSConcreteStackBlock
- _NSConcreteGlobalBlock
- _NSConcreteMallocBlock
這三種 Block 類的例項設定在不同的記憶體區域,_NSConcreteStackBlock 的例項設定在 stack 上,_NSConcreteGlobalBlock 的例項設定在 data segment(一般用來放置已初始化的全域性變數),_NSConcreteMallocBlock 的例項設定在 heap。如果 Block 在記述全域性變數的地方被設定或者 Block 沒有捕獲外部變數,那就生成一個 _NSConcreteGlobalBlock 例項。其它情況都會生成一個 _NSConcreteStackBlock 例項,也就是說,它是在棧上的,所以一旦它所屬的變數超出了變數作用域,該 Block 就被廢棄了。而當發生以下任一情況時:
- 手動呼叫 Block 的例項方法
copy
- Block 作為函式返回值返回
- 將 Block 賦值給附有
__strong
修飾符的成員變數 - 在方法名中含有
usingBlock
的 Cocoa 框架方法或 GCD 的 API 中傳遞 Block
如果此時 Block 在棧上,那就複製一份到堆上,並將複製得到的 Block 例項的isa
指標設為 _NSConcreteMallocBlock:
1 |
imply.isa = &__NSConcreteMallocBlock; |
而如果此時 Block 已經在堆上,那就把該 Block 的引用計數加1。
解答疑惑一
說到這裡,已經可以回答上文的第一個疑惑了。把 Block 賦值給self.handler
的時候,在棧上生成的 Block 被複制了一份,放到堆上,並被_handler
持有。而之後如果你把這個 Block 當作 GCD 引數使用(比較常見的需要使用 weak-strong dance 的情況),GCD 函式內部會把該 Block 再 copy 一遍,而此時 Block 已經在堆上,則該 Block 的引用計數加1。所以此時 Block 的引用計數是大於1的,即使self
物件被廢棄(譬如執行了退出當前頁面之類的操作),Block 會被 release 一次,但它的引用計數仍然大於0,故而不會被廢棄。
捕獲物件變數
Block 捕獲外部變數其實可分為三種情況:
- 捕獲變數的瞬時值
- 捕獲
__block
變數 - 捕獲物件
前兩種情況跟今天的主題關係不大,先按下不表。第三種情況,也就是本文所舉例子的情況,如果不用__weak
,而是直接捕獲self
的話,程式碼大概是這個樣子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; struct __xx_block_impl_y { struct __block_impl impl; OCClass *occlass; // 物件型變數不能作為 C 語言結構體成員,可能還需要做一些型別轉換,而且真實生成的程式碼並不一定叫 occlass,領會精神…… // ... }; static void __xx_block_func_y(struct __xx_block_impl_y *__cself) { OCClass *occlass = __cself -> occlass; // ... } // ... |
也就是說,表示 Block 例項的結構體中會多出一個OCClass
型別的成員變數,它會在結構體初始化時被賦值。而結構體中的函式指標void *FuncPtr
顯然是用來存放真正的 Block 操作的,它會在結構體初始化的時候被賦值為__xx_block_func_y
,__xx_block_func_y
以表示 Block 物件的結構體例項為引數,從而得到occlass
這個物件(即被捕獲的self
)。顯然,這裡會導致迴圈引用,而使用了__weak
之後,表示 Block 物件的結構體中的成員變數occlass
也將附有__weak
修飾符:
1 |
__weak OCClass *occlass; |
順便說一下,__weak
修飾的變數不會持有物件,它用一張 weak 表(類似於引用計數表的雜湊表)來管理物件和變數。賦值的時候它會以賦值物件的地址作為 key,變數的地址為 value,註冊到 weak 表中。一旦該物件被廢棄,就通過物件地址在 weak 表中找到變數的地址,賦值為 nil,然後將該條記錄從 weak 表中刪除。
那當我們使用 weak-strong dance 的時候是怎麼個情況呢,會再次持有物件從而造成迴圈引用麼?程式碼大致如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; struct __xx_block_impl_y { struct __block_impl impl; __weak OCClass *occlass; // ... }; static void __xx_block_func_y(struct __xx_block_impl_y *__cself) { OCClass *occlass = __cself -> occlass; // ... } |
解答疑惑二
__weak
是個神奇的東西,每次使用__weak
變數的時候,都會取出該變數指向的物件並 retain,然後將該物件註冊到 autoreleasepool 中。通過上述程式碼我們可以發現,在__xx_block_func_y
中,區域性變數occlass
會持有捕獲的物件,然後物件會被註冊到 autoreleasepool。這是延長物件生命週期的關鍵,但這不會造成迴圈引用,當函式執行結束,變數occlass
超出作用域,過一會兒(一般一次 RunLoop 之後),物件就被釋放了。所以 weak-strong dance 的行為非常符合預期:延長捕獲物件的生命週期,一旦 Block 執行完,物件被釋放,而 Block 也會被釋放(如果被 GCD 之類的 API copy 過一次增加了引用計數,那最終也會被 GCD 釋放)。
額外好處
上文說了每使用一次_weak
變數就會把物件註冊到 autoreleasepool 中,所以如果短時間內大量使用_weak
變數的話,會導致註冊到 autoreleasepool 中的物件大量增加,佔用一定記憶體。而 weak-strong dance 恰好無意中解決了這個隱患,在執行 Block 時,把_weak
變數(weakSelf)賦值給一個臨時變數(strongSelf),之後一直都使用這個臨時變數,所以_weak
變數只使用了一次,也就只有一個物件註冊到 autoreleasepool 中。