block沒那麼難(三):block和物件的記憶體管理

發表於2016-06-20

本系列博文總結自《Pro Multithreading and Memory Management for iOS and OS X with ARC》


在上一篇文章中,我們講了很多關於 block 和基礎變數的記憶體管理,接著我們聊聊 block 和物件的記憶體管理,如 block 經常會碰到的迴圈引用問題等等。


獲取物件

照例先來段程式碼輕鬆下,瞧瞧 block 是怎麼獲取外部物件的

翻譯後的關鍵程式碼摘錄如下

在本例中,當變數變數作用域結束時,array 被廢棄,強引用失效,NSMutableArray 類的例項物件會被釋放並廢棄。在這危難關頭,block 及時呼叫了 copy 方法,在 _Block_object_assign 中,將 array 賦值給 block 成員變數並持有。所以上面程式碼可以正常執行,列印出來的 array count 依次遞增。

總結程式碼可正常執行的原因關鍵就在於 block 通過呼叫 copy 方法,持有了 __strong 修飾的外部變數,使得外部物件在超出其作用域後得以繼續存活,程式碼正常執行。

在以下情形中, block 會從棧拷貝到堆:

  • 當 block 呼叫 copy 方法時,如果 block 在棧上,會被拷貝到堆上;
  • 當 block 作為函式返回值返回時,編譯器自動將 block 作為 _Block_copy 函式,效果等同於 block 直接呼叫 copy 方法;
  • 當 block 被賦值給 __strong id 型別的物件或 block 的成員變數時,編譯器自動將 block 作為 _Block_copy 函式,效果等同於 block 直接呼叫 copy 方法;
  • 當 block 作為引數被傳入方法名帶有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API 時。這些方法會在內部對傳遞進來的 block 呼叫 copy_Block_copy 進行拷貝;

其實後三種情況在上篇文章block的自動拷貝已經做過說明

除此之外,都需要手動呼叫。

延伸閱讀:Objective-C 結構體中的 __strong 成員變數

注意到 __main_block_impl_0 結構體有什麼異常沒?在 C 結構體中出現了 __strong 關鍵字修飾的變數。

通常情況下, Objective-C 的編譯器因為無法檢測 C 結構體初始化和釋放的時間,不能進行有效的記憶體管理,所以 Objective-C 的 C 結構體成員是不能用 __strong__weak 等等這類關鍵字修飾。然而 runtime 庫是可以在執行時檢測到 block 的記憶體變化,如 block 何時從棧拷貝到堆,何時從堆上釋放等等,所以就會出現上述結構體成員變數用 __strong 修飾的情況。


__block 變數和物件

__block 說明符可以修飾任何型別的自動變數。下面讓我們再看個小例子,啊,愉快的程式碼時間又到啦。

ARC 下,物件所有權修飾符預設為 __strong,即

__block id __strong obj 的作用和 id __strong obj 的作用十分類似。當 __block id __strong obj 從棧上拷貝到堆上時,_Block_object_assign 被呼叫,block 持有 obj;當 __block id __strong obj 從堆上被廢棄時,_Block_object_dispose 被呼叫用以釋放此物件,block 引用消失。

所以,只要是堆上的 __strong 修飾符修飾的 __block 物件型別的變數,和 block 內獲取到的 __strong 修飾符修飾的物件型別的變數,編譯器都能對它們的記憶體進行適當的管理。

如果上面的 __strong 換成 __weak,結果會怎樣呢?

結果是:

原因很簡單,array2 是弱引用,當變數作用域結束,array 所指向的物件記憶體被釋放,array2 指向 nil,向 nil 物件傳送 count 訊息就返回結果 0 了。

如果 __weak 再改成 __unsafe_unretained 呢?__unsafe_unretained 修飾的物件變數指標就相當於一個普通指標。使用這個修飾符有點需要注意的地方是,當指標所指向的物件記憶體被釋放時,指標變數不會被置為 nil。所以當使用這個修飾符時,一定要注意不要通過懸掛指標(指向被廢棄記憶體的指標)來訪問已經被廢棄的物件記憶體,否則程式就會崩潰。

如果 __unsafe_unretained 再改成 __autoreleasing 會怎樣呢?會報錯,編譯器並不允許你這麼幹!如果你這麼寫

編譯器就會報下面的錯誤,意思就是 __block__autoreleasing 不能同時使用。

error: __block variables cannot have __autoreleasing ownership __block id __autoreleasing obj = [[NSObject alloc] init];

迴圈引用

千辛萬苦,重頭戲終於來了。block 如果使用不小心,就容易出現迴圈引用,導致記憶體洩露。到底哪裡洩露了呢?通過前面的學習,各位童鞋應該有個底了,下面就讓我們一起進入這洩露地區瞧瞧,哪兒出了問題!

愉快的程式碼時間到

由於 self__strong 修飾,在 ARC 下,當編譯器自動將程式碼中的 block 從棧拷貝到堆時,block 會強引用和持有 self,而 self 恰好也強引用和持有了 block,就造成了傳說中的迴圈引用。

31

由於迴圈引用的存在,造成在 main() 函式結束時,記憶體仍然無法釋放,即記憶體洩露。編譯器也會給出警告資訊

 

為了避免這種情況發生,可以在變數宣告時用 __weak 修飾符修飾變數 self,讓 block 不強引用 self,從而破除迴圈。iOS4 和 Snow Leopard 由於對 weak 的支援不夠完全,可以用 __unsafe_unretained 代替。

32

再看一個例子

上面的例子中,雖然沒有直接使用 self,卻也存在迴圈引用的問題。因為對於編譯器來說,obj_ 就相當於 self->obj_,所以上面的程式碼就會變成

所以這個例子只要用 __weak,在 init 方法裡面加一行即可

破解迴圈引用還有一招,使用 __block 修飾物件,在 block 內將物件置為 nil 即可,如下

這個例子挺有意思的,如果執行 execBlock 方法,就沒有迴圈引用,如果不執行就有迴圈引用,挺值得玩味的。一方面,使用 __block 挺危險的,萬一程式碼中不執行 block ,就造成了迴圈引用,而且編譯器還沒法檢查出來;另一方面,使用 __block 可以讓我們通過 __block 變數去控制物件的生命週期,而且有可能在一些非常老舊的 MRC 程式碼中,由於不支援 __weak,我們可以使用此方法來代替 __unsafe_unretained,從而避免懸掛指標的問題。

還有個值得一提的時,在 MRC 下,使用 __block 說明符也可以避免迴圈引用。因為當 block 從棧拷貝到堆時,__block 物件型別的變數不會被 retain,沒有 __block 說明符的物件型別的變數則會被 retian。正是由於 __block 在 ARC 和 MRC 下的巨大差異,我們在寫程式碼時一定要區分清楚到底是 ARC 還是 MRC。

儘管 ARC 已經如此普及,我們可能已經可以不用去管 MRC 的東西,但要有點一定要明白,ARC 和 MRC 都是基於引用計數的記憶體管理,其本質上是一個東西,只不過 ARC 在編譯期自動化的做了記憶體引用計數的管理,使得系統可以在適當的時候保留記憶體,適當的時候釋放記憶體。

迴圈引用到此為止,東西並不多。如果明白了之前的知識點,就會了解迴圈引用不過是前面知識點的自然延伸點罷了。

Copy 和 Release

在 ARC 下,有時需要手動拷貝和釋放 block。在 MRC 下更是如此,可以直接用 copyrelease 來拷貝和釋放

拷貝到堆後,就可以 用 retain 持有 block

然而如果 block 在棧上,使用 retain 是毫無效果的,因此推薦使用 copy 方法來持有 block。

block 是 C 語言的擴充套件,所以可以在 C 中使用 block 的語法。比如,在上面的例子中,可以直接使用 Block_copyBlock_release 函式來代替 copyrelease 方法

Block_copy 的作用相當於之前看到過的 _Block_copy 函式,而且 Objective-C runtime 庫在執行時拷貝 block 用的就是這個函式。同理,釋放 block 時,runtime 呼叫了 Block_release 函式。

最後這裡有一篇總結 block 的文章的很不錯,推薦大家看看:http://tanqisen.github.io/blog/2013/04/19/gcd-block-cycle-retain/

相關文章