理清 Block 底層結構及其捕獲行為

Block 的本質
本質
- Block 的本質是一個 Objective-C 物件,它內部也擁有一個 isa 指標。
- Block 是封裝了函式及其呼叫環境的 Objective-C 物件
底層資料結構
一個簡單示例:
int main(int argc, const char * argv[]) { void (^block)(void) = ^{ NSLog(@"hey"); }; block(); return 0; } 複製程式碼
將以上 Objective-C 原始碼轉換成 c++ 相關原始碼,使用命令列 : xcrun -sdk iphoneos xclang -arch arm64 -rewrite-objc 檔名
c++ 的結構體與一般的類相似。
int main(int argc, const char * argv[]) { void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); return 0; } 複製程式碼
其中 Block 的資料結構為:
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; }; 複製程式碼
impl 變數資料結構:
struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; 複製程式碼
FuncPtr:函式實際呼叫的地址,因為 Block 可看作是捕獲自動變數的匿名函式。
Desc 變數資料結構:
static struct __main_block_desc_0 { size_t reserved; size_t Block_size; } 複製程式碼
Block 的型別
Objective-C 中 Block 有三種類型,其最終型別都是 NSBlock 。
- NSGlobalBlock (_NSConcreteGlobalBlock)
- NSStackBlock (_NSConcreteStackBlock)
- NSMallocBlock (_NSConcreteMallocBlock)
Block 型別的不同,主要根據捕獲變數的不同行為產生:
Block 型別 | 行為 |
---|---|
NSGlobalBlock | 沒有訪問 auto 變數 |
NSStackBlock | 訪問 auto 變數 |
NSMallocBlock | NSStackBlock 呼叫 copy |
在記憶體中的儲存位置

記憶體五大區:棧、堆、靜態區(BSS 段)、常量區(資料段)、程式碼段
copy 行為
不同型別的 Block 呼叫 copy 操作,也會產生不同的複製效果:
Block 型別 | 副本源的配置儲存域 | 複製效果 |
---|---|---|
__NSConcreteStackBlock | 棧 | 從棧複製到堆 |
__NSConcreteGlobalBlock | 資料段(常量區) | 什麼也不做 |
__NSConcreteMallocBlock | 堆 | 引用計數增加 |
- 在 ARC 環境下,編譯器會在以下情況自動將棧上的 Block 複製到堆上:
- Block 作為函式返回值
- 將 Block 賦值給 __strong 指標
- 蘋果 Cocoa、GCD 等 api 中方法引數是 block 型別
在 ARC 環境下,宣告的 block 屬性用 copy 或 strong 修飾的效果是一樣的,但在 MRC 環境下,則用 copy 修飾。
捕獲變數
為了保證在 Block 內部能夠正常訪問外部變數,Block 有一套變數捕獲機制:
變數型別 | 是否捕獲到 Block 內部 | 訪問方式 |
---|---|---|
區域性 auto 變數 | 是 | 值傳遞 |
區域性 static 變數 | 是 | 指標傳遞 |
全域性變數 | 否 | 直接訪問 |
若區域性 static 變數是基礎型別 int val
,則訪問方式為 int *val
若區域性 static 變數是物件型別 JAObject *obj
,則訪問方式為 JAObject **obj
基礎型別變數
一個簡單示例:
int age = 10; // static int age = 10; void (^block)(void) = ^{ NSLog(@"age is %d", age); }; block(); 複製程式碼
- 捕獲區域性 auto 基礎型別變數生成的 Block 結構體 struct __main_block_impl_0 變為:
struct __main_block_impl_0 { ··· int age; // 傳遞值 } 複製程式碼
- 捕獲區域性 static 基礎型別變數生成的 Block 結構體 struct __main_block_impl_0 變為:
struct __main_block_impl_0 { ··· int *age; // 傳遞指標 } 複製程式碼
- 捕獲全域性基礎型別變數生成的結構體 struct __main_block_impl_0 沒有包含 age ,因為作用域為全域性,可直接訪問。
物件型別變數
一個簡單示例:
JAPerson *person = [[JAPerson alloc] init]; person.age = 10; void (^block)(void) = ^{ NSLog(@"age is %d", person.age); }; block(); 複製程式碼
- 捕獲區域性 auto 物件型別變數生成的 Block 結構體 struct __main_block_impl_0 變為:
struct __main_block_impl_0 { ··· JAPerson *person; } 複製程式碼
- 捕獲區域性 static 物件型別變數生成的 Block 結構體 struct __main_block_impl_0 變為:
struct __main_block_impl_0 { ··· JAPerson **person; } 複製程式碼
- 捕獲全域性物件型別變數生成的結構體 struct __main_block_impl_0 沒有包含 person ,因為作用域為全域性,可直接訪問。
copy 和 dispose 函式
當捕獲的變數是物件型別或者使用 __Block 將變數包裝成一個 __Block_byref_變數名_0 型別的 Objective-C 物件時,會產生 copy
和 dispose
函式。
一個簡單示例:
JAPerson *person = [[JAPerson alloc] init]; person.age = 10; void (^block)(void) = ^{ NSLog(@"age is %d", person.age); }; block(); 複製程式碼
其中生成的 Block 的資料結構中多了 JAPerson 型別指標變數 person :
struct __main_block_impl_0 { ··· JAPerson *person; } 複製程式碼
Desc 變數資料結構多了記憶體管理相關的函式:
static struct __main_block_desc_0 { ··· void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*); void (*dispose)(struct __main_block_impl_0*); } 複製程式碼
這兩個函式的呼叫時機:
函式 | 呼叫時機 |
---|---|
copy | 棧上的 Block 複製到堆時 |
dispose | 堆上的 Block 被廢棄時 |
copy 和 dispose 底層相關原始碼
// 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); // Lose the reference, and if heap based and last reference, recover the memory BLOCK_EXPORT void _Block_release(const void *aBlock) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2); // Used by the compiler. Do not call this function yourself. BLOCK_EXPORT void _Block_object_assign(void *, const void *, const int) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2); // Used by the compiler. Do not call this function yourself. BLOCK_EXPORT void _Block_object_dispose(const void *, const int) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2); 複製程式碼
當 Block 內部訪問了物件型別的 auto 變數時:
- 如果 Block 是在棧上,將不會對 auto 變數產生強引用。
- 如果 Block 被拷貝到堆上,會呼叫 Block 內部的
copy
函式,copy
函式內部會呼叫_Block_object_assign
函式,_Block_object_assign
函式會根據 auto 變數的修飾符(__strong、__weak、__unsafe_unretain)作出相應的記憶體管理操作。
注意:若此時變數型別為物件型別,這裡僅限於 ARC 時會 retain ,MRC 時不會 retain 。
- 如果 Block 從堆上移除,會呼叫 Block 內部的
dispose
函式,dispose
函式內部會呼叫_Block_object_dispose
函式,_Block_object_dispose
函式會自動 release 引用的 auto 變數。
使用 __weak 修飾的 OC 程式碼轉換對應的 c++ 程式碼會報錯: error: cannot create __weak reference because the current deployment target does not support weak references
此時終端命令需支援 ARC 並指定 Runtime 版本: xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
記憶體管理
修改區域性 auto 變數
區域性 static 變數(指標訪問)、全域性變數(直接訪問)都可以在 Block 內部直接修改捕獲的變數,而區域性 auto 變數則主要通過使用 __block 儲存域修飾符來修改捕獲的變數。
- __block 修飾符可以用於解決 Block 內部無法修改區域性 auto 變數值的問題
- __block 修飾符不能用於修飾全域性變數、靜態變數(static)
編譯器會將 __block 修飾的變數包裝成一個 Objective-C 物件。
一個簡單示例:
__block int age = 10; void (^block)(void) = ^{ NSLog(@"age is %d", age); }; block(); 複製程式碼
其中 Block 的資料結構多了一個 __Block_byref_age_0 型別的指標:
struct __main_block_impl_0 { ··· __Block_byref_age_0 *age; // by ref } 複製程式碼
__Block_byref_age_0 結構體:
struct __Block_byref_age_0 { void *__isa; __Block_byref_age_0 *__forwarding; int __flags; int __size; int age; // age 真正儲存的地方 }; 複製程式碼
兩個注意點:
-
- 此處指標 val 是指向 age 的指標,而第二個 val 指的是 age 的值。

-
- 原始碼裡面通過
age->__forwarding->age
的方式去取值,是因為這兩個 age 都可能仍在棧上,此時直接age->age
訪問會有問題,而 copy 操作時 __forwarding 會指向堆上的 __Block_byref_age_0 ,此時就算第一個 age 仍在棧上,通過age->__forwarding
會重新指向堆上的 __Block_byref_age_0 ,此時再訪問 age 便不會有問題age->__forwarding->age
。
- 原始碼裡面通過


__block 的記憶體管理
使用 __block 修飾符時的記憶體管理情況:
- 當 Block 儲存在棧上時,並不會對 __block 變數強引用。
- 當 Block 被 copy 到堆上時,會呼叫 Block 內部的
copy
函式,copy
函式會呼叫__main_block_copy_0
函式對 __block 變數產生一個強引用。如下圖


- 當 Block 從堆上被移除時,會呼叫 Block 內部的
dispose
函式,dispose
函式會呼叫_Block_object_dispose
函式自動release
__block 變數。如下圖


__weak 和 __block 修飾時的引用情況
-
- 僅用 __weak 修飾
一個簡單的示例:
JAPerson *person = [[JAPerson alloc] init]; person.age = 10; __weak typeof(person) weakPerson = person; void (^block)(void) = ^{ NSLog(@"person‘s age is %d", weakPerson.age); }; 複製程式碼

-
- 使用 __block __weak 修飾
一個簡單的示例:
JAPerson *person = [[JAPerson alloc] init]; person.age = 10; __block __weak typeof(person) weakPerson = person; void (^block)(void) = ^{ NSLog(@"person‘s age is %d", weakPerson.age); }; block(); return 0; 複製程式碼
