來自掘金 《理清 Block 底層結(jié)構(gòu)及其捕獲行為》
Block 的本質(zhì)
本質(zhì)
- Block 的本質(zhì)是一個 Objective-C 對象,它內(nèi)部也擁有一個 isa 指針夜畴。
- Block 是封裝了函數(shù)及其調(diào)用環(huán)境的 Objective-C 對象
底層數(shù)據(jù)結(jié)構(gòu)
一個簡單示例:
int main(int argc, const char * argv[]) {
void (^block)(void) = ^{
NSLog(@"hey");
};
block();
return 0;
}
將以上 Objective-C 源碼轉(zhuǎn)換成 c++ 相關(guān)源碼拖刃,使用命令行 :
xcrun -sdk iphoneos xclang -arch arm64 -rewrite-objc 文件名
c++ 的結(jié)構(gòu)體與一般的類相似。
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 的數(shù)據(jù)結(jié)構(gòu)為:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
};
impl 變量數(shù)據(jù)結(jié)構(gòu):
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
FuncPtr:函數(shù)實際調(diào)用的地址贪绘,因為 Block 可看作是捕獲自動變量的匿名函數(shù)兑牡。
Desc 變量數(shù)據(jù)結(jié)構(gòu):
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 類型的不同均函,主要根據(jù)捕獲變量的不同行為產(chǎn)生:
Block 類型 | 行為 |
---|---|
NSGlobalBlock | 沒有訪問 auto 變量 |
NSStackBlock | 訪問 auto 變量 |
NSMallocBlock | NSStackBlock 調(diào)用 copy |
在內(nèi)存中的存儲位置
內(nèi)存五大區(qū):棧、堆菱涤、靜態(tài)區(qū)(BSS 段)苞也、常量區(qū)(數(shù)據(jù)段)、代碼段
copy 行為
不同類型的 Block 調(diào)用 copy 操作粘秆,也會產(chǎn)生不同的復(fù)制效果:
Block 類型 | 副本源的配置存儲域 | 復(fù)制效果 |
---|---|---|
__NSConcreteStackBlock | 棧 | 從棧復(fù)制到堆 |
__NSConcreteGlobalBlock | 數(shù)據(jù)段(常量區(qū)) | 什么也不做 |
__NSConcreteMallocBlock | 堆 | 引用計數(shù)增加 |
- 在 ARC 環(huán)境下如迟,編譯器會在以下情況自動將棧上的 Block 復(fù)制到堆上:
- Block 作為函數(shù)返回值
- 將 Block 賦值給 __strong 指針
- 蘋果 Cocoa、GCD 等 api 中方法參數(shù)是 block 類型
在 ARC 環(huán)境下攻走,聲明的 block 屬性用 copy 或 strong 修飾的效果是一樣的殷勘,但在 MRC 環(huán)境下,則用 copy 修飾昔搂。
捕獲變量
為了保證在 Block 內(nèi)部能夠正常訪問外部變量玲销,Block 有一套變量捕獲機制:
變量類型 | 是否捕獲到 Block 內(nèi)部 | 訪問方式 |
---|---|---|
局部 auto 變量 | 是 | 值傳遞 |
局部 static 變量 | 是 | 指針傳遞 |
全局變量 | 否 | 直接訪問 |
若局部 static 變量是基礎(chǔ)類型
int val
,則訪問方式為int *val
若局部 static 變量是對象類型JAObject *obj
摘符,則訪問方式為JAObject **obj
基礎(chǔ)類型變量
一個簡單示例:
int age = 10;
// static int age = 10;
void (^block)(void) = ^{
NSLog(@"age is %d", age);
};
block();
- 捕獲局部 auto 基礎(chǔ)類型變量生成的 Block 結(jié)構(gòu)體 struct __main_block_impl_0 變?yōu)?
struct __main_block_impl_0 {
···
int age; // 傳遞值
}
- 捕獲局部 static 基礎(chǔ)類型變量生成的 Block 結(jié)構(gòu)體 struct __main_block_impl_0 變?yōu)椋?/li>
struct __main_block_impl_0 {
···
int *age; // 傳遞指針
}
- 捕獲全局基礎(chǔ)類型變量生成的結(jié)構(gòu)體 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 結(jié)構(gòu)體 struct __main_block_impl_0 變?yōu)?
struct __main_block_impl_0 {
···
JAPerson *person;
}
- 捕獲局部 static 對象類型變量生成的 Block 結(jié)構(gòu)體 struct __main_block_impl_0 變?yōu)椋?/li>
struct __main_block_impl_0 {
···
JAPerson **person;
}
- 捕獲全局對象類型變量生成的結(jié)構(gòu)體 struct __main_block_impl_0 沒有包含 person 瘩绒,因為作用域為全局,可直接訪問别凹。
copy 和 dispose 函數(shù)
當(dāng)捕獲的變量是對象類型或者使用 __Block 將變量包裝成一個 _Block_byref變量名_0 類型的 Objective-C 對象時草讶,會產(chǎn)生 copy
和 dispose
函數(shù)。
一個簡單示例:
JAPerson *person = [[JAPerson alloc] init];
person.age = 10;
void (^block)(void) = ^{
NSLog(@"age is %d", person.age);
};
block();
其中生成的 Block 的數(shù)據(jù)結(jié)構(gòu)中多了 JAPerson 類型指針變量 person :
struct __main_block_impl_0 {
···
JAPerson *person;
}
Desc 變量數(shù)據(jù)結(jié)構(gòu)多了內(nèi)存管理相關(guān)的函數(shù):
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*);
}
這兩個函數(shù)的調(diào)用時機:
函數(shù) | 調(diào)用時機 |
---|---|
copy | 棧上的 Block 復(fù)制到堆時 |
dispose | 堆上的 Block 被廢棄時 |
copy 和 dispose 底層相關(guān)源碼
// 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);
當(dāng) Block 內(nèi)部訪問了對象類型的 auto 變量時:
- 如果 Block 是在棧上炉菲,將不會對 auto 變量產(chǎn)生強引用堕战。
- 如果 Block 被拷貝到堆上坤溃,會調(diào)用 Block 內(nèi)部的
copy
函數(shù),copy
函數(shù)內(nèi)部會調(diào)用_Block_object_assign
函數(shù)嘱丢,_Block_object_assign
函數(shù)會根據(jù) auto 變量的修飾符(__strong薪介、__weak、__unsafe_unretain)作出相應(yīng)的內(nèi)存管理操作越驻。
注意:若此時變量類型為對象類型汁政,這里僅限于 ARC 時會 retain ,MRC 時不會 retain 缀旁。
- 如果 Block 從堆上移除记劈,會調(diào)用 Block 內(nèi)部的
dispose
函數(shù),dispose
函數(shù)內(nèi)部會調(diào)用_Block_object_dispose
函數(shù)并巍,_Block_object_dispose
函數(shù)會自動 release 引用的 auto 變量目木。
使用 __weak 修飾的 OC 代碼轉(zhuǎn)換對應(yīng)的 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
內(nèi)存管理
修改局部 auto 變量
局部 static 變量(指針訪問)、全局變量(直接訪問)都可以在 Block 內(nèi)部直接修改捕獲的變量懊渡,而局部 auto 變量則主要通過使用 __block 存儲域修飾符來修改捕獲的變量刽射。
- __block 修飾符可以用于解決 Block 內(nèi)部無法修改局部 auto 變量值的問題
- __block 修飾符不能用于修飾全局變量、靜態(tài)變量(static)
編譯器會將 __block 修飾的變量包裝成一個 Objective-C 對象剃执。
一個簡單示例:
__block int age = 10;
void (^block)(void) = ^{
NSLog(@"age is %d", age);
};
block();
其中 Block 的數(shù)據(jù)結(jié)構(gòu)多了一個 __Block_byref_age_0 類型的指針:
struct __main_block_impl_0 {
···
__Block_byref_age_0 *age; // by ref
}
__Block_byref_age_0 結(jié)構(gòu)體:
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 的內(nèi)存管理
使用 __block 修飾符時的內(nèi)存管理情況:
- 當(dāng) Block 存儲在棧上時,并不會對 __block 變量強引用低千。
- 當(dāng) Block 被 copy 到堆上時配阵,會調(diào)用 Block 內(nèi)部的
copy
函數(shù),copy
函數(shù)會調(diào)用__main_block_copy_0
函數(shù)對 __block 變量產(chǎn)生一個強引用示血。如下圖
- 當(dāng) Block 從堆上被移除時棋傍,會調(diào)用 Block 內(nèi)部的
dispose
函數(shù),dispose
函數(shù)會調(diào)用_Block_object_dispose
函數(shù)自動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;
循環(huán)引用
常見的循環(huán)引用問題:
ARC 環(huán)境下解決循環(huán)引用
- 弱引用持有:使用 __weak 或 __unsafe__unretain 解決
- 手動將一方置為 nil :使用 __block 解決瘫拣,在 block 內(nèi)部將一方置為 nil ,因此必須執(zhí)行該 block
MRC 環(huán)境下解決循環(huán)引用
- 弱引用持有:使用 __unsafe__unretain 解決
- 直接使用 __block 解決告喊,無需手動將一方置為 nil 麸拄,因為底層
_Block_object_assign
函數(shù)在 MRC 環(huán)境下對 block 內(nèi)部的對象不會進(jìn)行 retain 操作派昧。
- 直接使用 __block 解決告喊,無需手動將一方置為 nil 麸拄,因為底層