本系列博文總結自《Pro Multithreading and Memory Management for iOS and OS X with ARC》
在上一篇文章中,我們講了很多關於 block 和基礎變數的記憶體管理,接著我們聊聊 block 和物件的記憶體管理,如 block 經常會碰到的迴圈引用問題等等。
獲取物件
照例先來段程式碼輕鬆下,瞧瞧 block 是怎麼獲取外部物件的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
/********************** capturing objects **********************/ typedef void (^blk_t)(id obj); blk_t blk; - (void)viewDidLoad { [self captureObject]; blk([[NSObject alloc] init]); blk([[NSObject alloc] init]); blk([[NSObject alloc] init]); } - (void)captureObject { id array = [[NSMutableArray alloc] init]; blk = [^(id obj) { [array addObject:obj]; NSLog(@"array count = %ld", [array count]); } copy]; } |
翻譯後的關鍵程式碼摘錄如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
/* a struct for the Block and some functions */ struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0 *Desc; id __strong array; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id __strong _array, int flags=0) : array(_array) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) { id __strong array = __cself->array; [array addObject:obj]; NSLog(@"array count = %ld", [array count]); } static void __main_block_copy_0(struct __main_block_impl_0 *dst, __main_block_impl_0 *src) { _Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT); } static void __main_block_dispose_0(struct __main_block_impl_0 *src) { _Block_object_dispose(src->array, BLOCK_FIELD_IS_OBJECT); } struct static struct __main_block_desc_0 { unsigned long reserved; unsigned long Block_size; void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*); void (*dispose)(struct __main_block_impl_0*); } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0 }; /* Block literal and executing the Block */ blk_t blk; { id __strong array = [[NSMutableArray alloc] init]; blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, array, 0x22000000); blk = [blk copy]; } (*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]); (*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]); (*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]); |
在本例中,當變數變數作用域結束時,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 說明符可以修飾任何型別的自動變數。下面讓我們再看個小例子,啊,愉快的程式碼時間又到啦。
1 2 |
/******* block 修飾物件 *******/ __block id obj = [[NSObject alloc] init]; |
ARC 下,物件所有權修飾符預設為 __strong
,即
1 |
__block id __strong obj = [[NSObject alloc] init]; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
/******* block 修飾物件轉換後的程式碼 *******/ /* struct for __block variable */ struct __Block_byref_obj_0 { void *__isa; __Block_byref_obj_0 *__forwarding; int __flags; int __size; void (*__Block_byref_id_object_copy)(void*, void*); void (*__Block_byref_id_object_dispose)(void*); __strong id obj; }; static void __Block_byref_id_object_copy_131(void *dst, void *src) { _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131); } static void __Block_byref_id_object_dispose_131(void *src) { _Block_object_dispose(*(void * *) ((char*)src + 40), 131); } /* __block variable declaration */ __Block_byref_obj_0 obj = { 0, &obj, 0x2000000, sizeof(__Block_byref_obj_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, [[NSObject alloc] init] }; |
__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
,結果會怎樣呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/********************** capturing __weak objects **********************/ typedef void (^blk_t)(id obj); blk_t blk; - (void)viewDidLoad { [self captureObject]; blk([[NSObject alloc] init]); blk([[NSObject alloc] init]); blk([[NSObject alloc] init]); } - (void)captureObject { id array = [[NSMutableArray alloc] init]; id __weak array2 = array; blk = [^(id obj) { [array2 addObject:obj]; NSLog(@"array2 count = %ld", [array2 count]); } copy]; } |
結果是:
1 2 3 |
array2 count = 0 array2 count = 0 array2 count = 0 |
原因很簡單,array2
是弱引用,當變數作用域結束,array
所指向的物件記憶體被釋放,array2
指向 nil,向 nil 物件傳送 count
訊息就返回結果 0 了。
如果 __weak
再改成 __unsafe_unretained
呢?__unsafe_unretained
修飾的物件變數指標就相當於一個普通指標。使用這個修飾符有點需要注意的地方是,當指標所指向的物件記憶體被釋放時,指標變數不會被置為 nil。所以當使用這個修飾符時,一定要注意不要通過懸掛指標(指向被廢棄記憶體的指標)來訪問已經被廢棄的物件記憶體,否則程式就會崩潰。
如果 __unsafe_unretained
再改成 __autoreleasing
會怎樣呢?會報錯,編譯器並不允許你這麼幹!如果你這麼寫
1 |
__block id __autoreleasing obj = [[NSObject alloc] init]; |
編譯器就會報下面的錯誤,意思就是 __block
和 __autoreleasing
不能同時使用。
error: __block variables cannot have __autoreleasing ownership __block id __autoreleasing obj = [[NSObject alloc] init];
迴圈引用
千辛萬苦,重頭戲終於來了。block 如果使用不小心,就容易出現迴圈引用,導致記憶體洩露。到底哪裡洩露了呢?通過前面的學習,各位童鞋應該有個底了,下面就讓我們一起進入這洩露地區瞧瞧,哪兒出了問題!
愉快的程式碼時間到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// ARC enabled /************** MyObject Class **************/ typedef void (^blk_t)(void); @interface MyObject : NSObject { blk_t blk_; } @end @implementation MyObject - (id)init { self = [super init]; blk_ = ^{NSLog(@"self = %@", self);}; return self; } - (void)dealloc { NSLog(@"dealloc"); } @end /************** main function **************/ int main() { id myObject = [[MyObject alloc] init]; NSLog(@"%@", myObject); return 0; } |
由於 self
是 __strong
修飾,在 ARC 下,當編譯器自動將程式碼中的 block 從棧拷貝到堆時,block 會強引用和持有 self
,而 self
恰好也強引用和持有了 block,就造成了傳說中的迴圈引用。
由於迴圈引用的存在,造成在 main()
函式結束時,記憶體仍然無法釋放,即記憶體洩露。編譯器也會給出警告資訊
12345 warning: capturing 'self' strongly in this block is likely to lead to a retain cycle [-Warc-retain-cycles]blk_ = ^{NSLog(@"self = %@", self);};note: Block will be retained by an object strongly retained by the captured objectblk_ = ^{NSLog(@"self = %@", self);};
為了避免這種情況發生,可以在變數宣告時用 __weak
修飾符修飾變數 self
,讓 block 不強引用 self
,從而破除迴圈。iOS4 和 Snow Leopard 由於對 weak 的支援不夠完全,可以用 __unsafe_unretained
代替。
1 2 3 4 5 6 7 |
- (id)init { self = [super init]; id __weak tmp = self; blk_ = ^{NSLog(@"self = %@", tmp);}; return self; } |
再看一個例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@interface MyObject : NSObject { blk_t blk_; id obj_; } @end @implementation MyObject - (id)init { self = [super init]; blk_ = ^{ NSLog(@"obj_ = %@", obj_); }; return self; } ... ... @end |
上面的例子中,雖然沒有直接使用 self,卻也存在迴圈引用的問題。因為對於編譯器來說,obj_
就相當於 self->obj_
,所以上面的程式碼就會變成
1 |
blk_ = ^{ NSLog(@"obj_ = %@", self->obj_); }; |
所以這個例子只要用 __weak
,在 init
方法裡面加一行即可
1 |
id __weak obj = obj_; |
破解迴圈引用還有一招,使用 __block 修飾物件,在 block 內將物件置為 nil 即可,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
typedef void (^blk_t)(void); @interface MyObject : NSObject { blk_t blk_; } @end @implementation MyObject - (id)init { self = [super init]; __block id tmp = self; blk_ = ^{ NSLog(@"self = %@", tmp); tmp = nil; }; return self; } - (void)execBlock { blk_(); } - (void)dealloc { NSLog(@"dealloc"); } @end int main() { id object = [[MyObject alloc] init]; [object execBlock]; return 0; } |
這個例子挺有意思的,如果執行 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 下更是如此,可以直接用 copy
和 release
來拷貝和釋放
1 2 |
void (^blk_on_heap)(void) = [blk_on_stack copy]; [blk_on_heap release]; |
拷貝到堆後,就可以 用 retain
持有 block
1 |
[blk_on_heap retain]; |
然而如果 block 在棧上,使用 retain
是毫無效果的,因此推薦使用 copy
方法來持有 block。
block 是 C 語言的擴充套件,所以可以在 C 中使用 block 的語法。比如,在上面的例子中,可以直接使用 Block_copy
和 Block_release
函式來代替 copy
和 release
方法
1 2 |
void (^blk_on_heap)(void) = Block_copy(blk_on_stack); Block_release(blk_on_heap); |
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/