本系列博文總結自《Pro Multithreading and Memory Management for iOS and OS X with ARC》
瞭解了 block的實現,我們接著來聊聊 block 和變數的記憶體管理。本文將介紹可寫變數、block的記憶體段、__block變數的記憶體段等內容,看完本文會對 block 和變數的記憶體管理有更加清晰的認識。
上篇文章舉了個例子,在 block 內獲取了一個外部的區域性變數,可以讀取,但無法進行寫入的修改操作。在 C 語言中有三種型別的變數,可在 block 內進行讀寫操作
- 全域性變數
- 全域性靜態變數
- 靜態變數
全域性變數
和 全域性靜態變數
由於作用域在全域性,所以在 block 內訪問和讀寫這兩類變數和普通函式沒什麼區別,而 靜態變數
作用域在 block 之外,是怎麼對它進行讀寫呢?通過 clang 工具,我們發現原來 靜態變數
是通過指標傳遞,將變數傳遞到 block 內,所以可以修改變數值。而上篇文章中的外部變數是通過值傳遞,自然沒法對獲取到的外部變數進行修改。由此,可以給我們一個啟示,當我們需要修改外部變數時,是不是也可以像 靜態變數
這樣通過指標來修改外部變數的值呢?
Apple 早就為我們準備了這麼一個東西 —— “__block”
__block 說明符
按照慣例,重寫一小段程式碼看看 __block 的真身
1 2 3 4 5 6 7 8 9 |
/************* 使用 __block 的原始碼 *************/ int main() { __block int intValue = 0; void (^blk)(void) = ^{ intValue = 1; }; return 0; } |
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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
/************* 使用 clang 翻譯後如下 *************/ struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; struct __Block_byref_intValue_0 { void *__isa; __Block_byref_intValue_0 *__forwarding; int __flags; int __size; int intValue; }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __Block_byref_intValue_0 *intValue; // by ref __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_intValue_0 *_intValue, int flags=0) : intValue(_intValue->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { __Block_byref_intValue_0 *intValue = __cself->intValue; // bound by ref (intValue->__forwarding->intValue) = 1; } static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src) { _Block_object_assign((void*)&dst->intValue, (void*)src->intValue, 8/*BLOCK_FIELD_IS_BYREF*/); } static void __main_block_dispose_0(struct __main_block_impl_0 *src) { _Block_object_dispose((void*)src->intValue, 8/*BLOCK_FIELD_IS_BYREF*/); } static struct __main_block_desc_0 { size_t reserved; size_t 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 }; int main() { __attribute__((__blocks__(byref))) __Block_byref_intValue_0 \ intValue = { (void*)0, (__Block_byref_intValue_0 *)&intValue, 0, sizeof(__Block_byref_intValue_0), 0 }; void (*blk)(void) = (void (*)()) &__main_block_impl_0 \ ( (void *)__main_block_func_0, \ &__main_block_desc_0_DATA, \ (__Block_byref_intValue_0 *)&intValue, \ 570425344 \ ); return 0; } |
在加了 __block 之後,程式碼量增加了不少,仔細檢視,其實只是比原來多了
123 __Block_byref_intValue_0 結構體:用於封裝 __block 修飾的外部變數。_Block_object_assign 函式:當 block 從棧拷貝到堆時,呼叫此函式。_Block_object_dispose 函式:當 block 從堆記憶體釋放時,呼叫此函式。
OC原始碼中的 __block intValue
翻譯後變成了 __Block_byref_intValue_0
結構體指標變數 intValue
,通過指標傳遞到 block 內,這與前面說的 靜態變數
的指標傳遞是一致的。除此之外,整體的執行流程與不加 __block 基本一致,不再贅述。但 __Block_byref_intValue_0
這個結構體需特別注意下
1 2 3 4 5 6 7 8 9 |
// 儲存 __block 外部變數的結構體 struct __Block_byref_intValue_0 { void *__isa; // 物件指標 __Block_byref_intValue_0 *__forwarding; // 指向自己的指標 int __flags; // 標誌位變數 int __size; // 結構體大小 int intValue; // 外部變數 }; |
在已有結構體指標指向 __Block_byref_intValue_0
時,結構體裡面還多了個 __forwarding
指向自己的指標變數,難道不顯得多餘嗎?一點也不,本文後面會闡述。
block 的記憶體管理
在前文中,已經提到了 block 的三種型別 NSConcreteGlobalBlock
、_NSConcreteStackBlock
、_NSConcreteMallocBlock
,見名知意,可以看出三種 block 在記憶體中的分佈
_NSConcreteGlobalBlock
12 1、當 block 字面量寫在全域性作用域時,即為 global block;2、當 block 字面量不獲取任何外部變數時,即為 global block;
除了上述描述的兩種情況,其他形式建立的 block 均為 stack block
。
1 2 3 4 5 6 |
// 下面 block 雖然定義在 for 迴圈內,但符合第二種情況,所以也是 global block typedef int (^blk_t)(int); for (int rate = 0; rate 10; ++rate) { blk_t blk = ^(int count){return rate * count;}; } |
_NSConcreteGlobalBlock
型別的 block 處於記憶體的 ROData 段,此處沒有區域性變數的騷擾,執行不依賴上下文,記憶體管理也簡單的多。
_NSConcreteStackBlock
_NSConcreteStackBlock
型別的 block 處於記憶體的棧區。global block
由於處在 data 段,可以通過指標安全訪問,但 stack block
處在記憶體棧區,如果其變數作用域結束,這個 block 就被廢棄,block 上的 __block 變數也同樣會被廢棄。
為了解決這個問題,block 提供了 copy 的功能,將 block 和 __block 變數從棧拷貝到堆,就是下面要說的 _NSConcreteMallocBlock
。
_NSConcreteMallocBlock
當 block 從棧拷貝到堆後,當棧上變數作用域結束時,仍然可以繼續使用 block
此時,堆上的 block 型別為 _NSConcreteMallocBlock
,所以會將 _NSConcreteMallocBlock
寫入 isa
impl.isa = &_NSConcreteMallocBlock;
如果你細心的觀察上面的轉換後的程式碼,會發現訪問結構體 __Block_byref_intValue_0
內部的成員變數都是通過訪問 __forwarding
指標完成的。為了保證能正確訪問棧上的 __block 變數,進行 copy 操作時,會將棧上的 __forwarding
指標指向了堆上的 block 結構體例項。
block 的自動拷貝和手動拷貝
在開啟 ARC 時,大部分情況下編譯器通常會將建立在棧上的 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 自動拷貝的例子
1 2 3 4 5 6 |
/************ ARC下編譯器自動拷貝block ************/ typedef int (^blk_t)(int); blk_t func(int rate) { return ^(int count){return rate * count;}; } |
上面的 block 獲取了外部變數,所以是建立在棧上,當 func
函式返回給呼叫者時,脫離了區域性變數 rate
的作用範圍,如果呼叫者使用這個 block 就會出問題。那 ARC 開啟的情況呢?執行這個 block 一切正常。和我們的預期結果不一樣,ARC 到底給 block 施了什麼魔法?我們將上面的程式碼翻譯下
1 2 3 4 5 6 |
blk_t func(int rate) { blk_t tmp = &__func_block_impl_0(__func_block_func_0, &__func_block_desc_0_DATA, rate); tmp = objc_retainBlock(tmp); return objc_autoreleaseReturnValue(tmp); } |
轉換後出現兩個新函式 objc_retainBlock
、objc_autoreleaseReturnValue
。如果你看過runtime 庫(點此下載) ,在 runtime/objc-arr.mm
檔案中就有這兩個函式的實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/*********** objc_retainBlock() 的實現 ***********/ id objc_retainBlock(id x) { #if ARR_LOGGING objc_arr_log("objc_retain_block", x); ++CompilerGenerated.blockCopies; #endif return (id)_Block_copy(x); } // Create a heap based copy of a Block or simply add a reference to an existing one. // This must be paired with Block_release to recover memory, even when running // under Objective-C Garbage Collection. BLOCK_EXPORT void *_Block_copy(const void *aBlock) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2); |
1 2 3 4 5 6 7 8 9 10 11 12 |
/*********** objc_autoreleaseReturnValue() 的實現 ***********/ id objc_autoreleaseReturnValue(id obj) { #if SUPPORT_RETURN_AUTORELEASE assert(_pthread_getspecific_direct(AUTORELEASE_POOL_RECLAIM_KEY) == NULL); if (callerAcceptsFastAutorelease(__builtin_return_address(0))) { _pthread_setspecific_direct(AUTORELEASE_POOL_RECLAIM_KEY, obj); return obj; } #endif return objc_autorelease(obj); } |
通過上面的程式碼和註釋,意思就很明顯了,由於 block 字面量是建立在棧記憶體,通過 objc_retainBlock()
函式拷貝到堆記憶體,讓 tmp
重新指向堆上的 block,然後將 tmp
所指的堆上的 block 作為一個 Objective-C 物件放入 autoreleasepool 裡面,從而保證了返回後的 block 仍然可以正確執行。
看完了 block 的自動拷貝,那麼看看在 ARC 下需要手動拷貝 block 的例子
1 2 3 4 5 6 7 8 |
/************ ARC下編譯器手動拷貝block ************/ - (id)getBlockArray { int val = 10; return [[NSArray alloc] initWithObjects: ^{NSLog(@"blk0:%d", val);}, ^{NSLog(@"blk1:%d", val);}, nil]; } |
關於 block 的拷貝操作可以用一張表總結下
block 拷貝的講解就到此為止,有興趣可以瞭解下 block 的多次拷貝。
123456789101112131415161718192021 block的多次拷貝:下面的例子在 ARC 下並不會產生記憶體洩露哦// block 多次拷貝原始碼blk = [[[[blk copy] copy] copy] copy];// 翻譯後的程式碼{blk_t tmp = [blk copy];blk = tmp;}{blk_t tmp = [blk copy];blk = tmp;}{blk_t tmp = [blk copy];blk = tmp;}{blk_t tmp = [blk copy];blk = tmp;}
__block 變數的記憶體管理
上面囉嗦一堆,這小節主要用圖說話,必要時加文字說明。
- 當 block 從棧記憶體被拷貝到堆記憶體時,__block 變數的變化如下圖。需要說明的是,當棧上的 block 被拷貝到堆上,堆上的 block 再次被拷貝時,對 __block 變數已經沒有影響了。
- 當多個 block 獲取同一個 __block 變數,block 從棧被拷貝到堆時
- 當 block 被廢棄時,__block 變數被釋放
- __forwarding
前文已經說過,當 block 從棧被拷貝到堆時,__forwarding
指標變數也會指向堆區的結構體。但是為什麼要這麼做呢?為什麼要讓原本指向棧區的結構體的指標,去指向堆區的結構體呢?看起來匪夷所思,實則原因很簡單,要從__forwarding
產生的緣由說起。想想起初為什麼要給 block 新增 copy 的功能,就是因為 block 獲取了區域性變數,當要在其他地方(超出區域性變數作用範圍)使用這個 block 的時候,由於訪問區域性變數異常,導致程式崩潰。為了解決這個問題,就給 block 新增了 copy 功能。在將 block 拷貝到堆上的同時,將__forwarding
指標指向堆上結構體。後面如果要想使用 __block 變數,只要通過__forwarding
訪問堆上變數,就不會出現程式崩潰了。
1 2 3 4 5 6 7 8 9 |
/*************** __forwarding 的作用 ***************/ //猜猜下面程式碼的列印結果? { __block int val = 0; void (^blk)(void) = [^{++val;} copy]; ++val; blk(); NSLog(@"%d", val); } |
一定有很多人會猜 1
,其實列印 2
。原因很簡單,當棧上的 block 被拷貝到堆上時,棧上的 __forwarding
也會指向堆上的 __block 變數的結構體。
上面的程式碼中 ^{++val;}
和 ++val;
都會被轉換成 ++(val.__forwarding->val);
,堆上的 val
被加了兩次,最後列印堆上的 val
為 2
。
圖解
block 和變數的記憶體管理終於講完了,看似很長,只要瞭解本質,其實很簡單。期待下篇文章《block沒那麼難(三):block和物件的記憶體管理》。