當(dāng)需要執(zhí)行異步操作,或同步多個(gè)操作時(shí),塊(Block)會(huì)非常有用。這一篇文章將介紹 Block 的本質(zhì)。如果你對(duì) block 還不了解,推薦先查看Block的用法。
1. Block的本質(zhì)
Block 是封裝了函數(shù)調(diào)用及函數(shù)調(diào)用環(huán)境的 Objective-C 對(duì)象,內(nèi)部也有一個(gè) isa 指針。即 Block 本質(zhì)上也是一個(gè) Objective-C 對(duì)象。
下面寫一個(gè)簡(jiǎn)單的 block:
int age = 10;
void(^myblock)(void) = ^{
NSLog(@"age: %d", age);
};
myblock();
使用 clang 命令將上述代碼轉(zhuǎn)化為 C++,方便查看 block 內(nèi)部結(jié)構(gòu):
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
轉(zhuǎn)化后如下:
int age = 10;
// 定義block變量
void(*myblock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
// 調(diào)用block
((void (*)(__block_impl *))((__block_impl *)myblock)->FuncPtr)((__block_impl *)myblock);
1.1 聲明 block

1.1.1 __main_block_impl_0
通過轉(zhuǎn)化的 C++ 代碼可以看到,block定義中調(diào)用了__main_block_impl_0函數(shù),并將其地址賦值給myblock。進(jìn)一步查看__main_block_impl_0函數(shù):
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
// 該構(gòu)造函數(shù)最終返回__main_block_impl_0。會(huì)將傳入的_age賦值給成員age。
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
__main_block_impl_0結(jié)構(gòu)體內(nèi)的構(gòu)造函數(shù)對(duì)變量進(jìn)行賦值,最終返回__main_block_impl_0結(jié)構(gòu)體,也就是最終返回給myblock變量的是__main_block_impl_0結(jié)構(gòu)體。
1.1.2 __main_block_func_0
__main_block_impl_0函數(shù)的第一個(gè)參數(shù)是__main_block_func_0,其定義如下:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_05_pj1lwvjs50j3gx6vjtvxcvf80000gn_T_main_63c9df_mi_0, age);
}
__main_block_func_0函數(shù)內(nèi)存儲(chǔ)著 block 內(nèi)代碼。其函數(shù)內(nèi)部先取出局部變量 age,后面調(diào)用NSLog。
也就是將 block 內(nèi)的代碼封裝到__main_block_func_0函數(shù),將__main_block_func_0函數(shù)地址傳遞給__main_block_impl_0。
1.1.3 __main_block_desc_0
__main_block_impl_0函數(shù)的第二個(gè)參數(shù)是__main_block_desc_0,其定義如下:
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
__main_block_desc_0中存儲(chǔ)著兩個(gè)成員,reserved和Block_size。并且為reserved賦值0,為Block_size賦值sizeof(struct __main_block_impl_0),即block的大小。
最終,將__main_block_desc_0結(jié)構(gòu)體傳給__main_block_impl_0中,賦值給desc。
1.1.4 age
__main_block_impl_0函數(shù)的第三個(gè)參數(shù)是age,即定義的局部變量。
如果在 block 中使用了局部變量,block 聲明的時(shí)候會(huì)將 age 作為參數(shù)傳入,即 block 會(huì)捕獲(capture)age。如果 block 中沒有使用 age,則只會(huì)給__main_block_impl_0函數(shù)傳入__main_block_func_0和__main_block_desc_0_DATA參數(shù)。
由于 block 在聲明時(shí)捕獲了局部變量,在聲明后、調(diào)用前修改局部變量值,不會(huì)影響 block 內(nèi)捕獲到的局部變量值。如下所示:
int age = 10;
void(^myblock)(void) = ^{
NSLog(@"age: %d", age);
};
age = 11;
myblock();
執(zhí)行后,控制臺(tái)打印如下:
age: 10
block 在定義之后已將局部變量age值存儲(chǔ)在__main_block_impl_0結(jié)構(gòu)體,調(diào)用時(shí)直接從結(jié)構(gòu)體中取出。聲明后修改局部變量的值不會(huì)影響__main_block_impl_0結(jié)構(gòu)體捕獲的值。
1.1.5 __block_impl
__main_block_impl_0結(jié)構(gòu)體第一個(gè)成員是__block_impl結(jié)構(gòu)體,__block_impl結(jié)構(gòu)體如下:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
isa指針指向類對(duì)象,FuncPtr指針存儲(chǔ)著__main_block_func_0函數(shù)地址,即 block 內(nèi)代碼地址。
__block_impl結(jié)構(gòu)體第一個(gè)成員就是 isa 指針。Objective-C對(duì)象本質(zhì)上也是結(jié)構(gòu)體,第一個(gè)成員也是 isa 指針。因此,block 本質(zhì)上也是一個(gè) OC 對(duì)象。__main_block_impl_0函數(shù)的構(gòu)造函數(shù)將傳入 block 的值存儲(chǔ)到__main_block_impl_0結(jié)構(gòu)體中,最終將__main_block_impl_0結(jié)構(gòu)體地址賦值給myblock。
分析__main_block_impl_0構(gòu)造函數(shù),特點(diǎn)如下:
-
__main_block_func_0封裝了函數(shù)地址,其中先取出局部變量,再調(diào)用 block 內(nèi)代碼。 -
__main_block_desc_0_DATA封裝 block 大小。 -
age是 block 捕獲的局部變量。 -
__main_block_impl_0結(jié)構(gòu)體中的__block_impl結(jié)構(gòu)體包含了isa指針、FuncPtr。
1.2 調(diào)用 block
// 調(diào)用block
((void (*)(__block_impl *))((__block_impl *)myblock)->FuncPtr)((__block_impl *)myblock);
將上述代碼中的強(qiáng)制轉(zhuǎn)換移除后,變?yōu)橄旅娴拇a:
(myblock->FuncPtr)(myblock);
調(diào)用myblock就是通過myblock找到FuncPtr指針,然后進(jìn)行調(diào)用。
myblock是指向__main_block_impl_0結(jié)構(gòu)體的指針,內(nèi)部并沒有FuncPtr指針,為什么這里可以直接訪問?這是因?yàn)?code>__main_block_impl_0結(jié)構(gòu)體第一個(gè)成員是__block_impl,而__block_impl也是一個(gè)結(jié)構(gòu)體,即__main_block_impl_0可以改為以下內(nèi)容:
struct __main_block_impl_0 {
// 使用__block_impl直接替換
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
struct __main_block_desc_0* Desc;
int age;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
另一方面,__main_block_impl_0結(jié)構(gòu)體第一個(gè)成員是__block_impl,__main_block_impl_0結(jié)構(gòu)體地址就是__block_impl的地址,這樣也可以查找到FuncPtr指針。
block 底層的數(shù)據(jù)結(jié)構(gòu)也可以使用下面圖片表示:

2. 變量捕獲
除了包含可執(zhí)行代碼,塊還具有捕獲塊以外值的能力。如果在一個(gè)方法內(nèi)聲明了一個(gè)塊,該塊可以獲取方法內(nèi)任何變量,也就是可以捕獲局部變量。

2.1 局部變量
2.1.1 auto變量
局部變量默認(rèn)是 automatic variable 類型,簡(jiǎn)寫為auto,一般省略不寫。當(dāng)程序進(jìn)入、離開局部變量作用域時(shí),會(huì)自動(dòng)分配、釋放內(nèi)存。
auto會(huì)自動(dòng)捕獲到 block 內(nèi),__main_block_impl_0結(jié)構(gòu)體內(nèi)增加了存儲(chǔ)局部變量的成員。block 內(nèi)訪問auto變量的方式是值傳遞,即直接將auto變量傳遞給__main_block_impl_0函數(shù)。
2.1.2 static變量
static變量會(huì)一直存儲(chǔ)在內(nèi)存中。block 會(huì)捕獲static修飾的局部變量,訪問時(shí)使用指針訪問。
下面分別添加使用auto、static修飾的局部變量:
auto int age = 10;
static int weight = 125;
void(^myblock)(void) = ^{
NSLog(@"age: %d, weight: %d", age, weight);
};
myblock();
生成C++后如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 捕獲了age、weight。
int age;
int *weight;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_weight, int flags=0) : age(_age), weight(_weight) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
int *weight = __cself->weight; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_05_pj1lwvjs50j3gx6vjtvxcvf80000gn_T_main_e2f202_mi_0, age, (*weight));
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
auto int age = 10;
static int weight = 125;
// age直接傳遞值,weight傳遞指針。
void(*myblock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &weight));
((void (*)(__block_impl *))((__block_impl *)myblock)->FuncPtr)((__block_impl *)myblock);
}
return 0;
}
可以看到,__main_block_impl_0捕獲了age、weight,并且給__main_block_impl_0函數(shù)傳遞age時(shí)直接傳遞值,傳遞weight時(shí)傳遞的是指針。
2.2 全局變量
block 是否會(huì)捕獲全局變量?以及如何使用?
添加以下全局變量:
int height = 170;
static int number = 11;
生成C++代碼如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
int *weight;
// 并沒有捕獲全局變量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_weight, int flags=0) : age(_age), weight(_weight) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
int *weight = __cself->weight; // bound by copy
// 直接使用height、number
NSLog((NSString *)&__NSConstantStringImpl__var_folders_05_pj1lwvjs50j3gx6vjtvxcvf80000gn_T_main_964e22_mi_0, age, (*weight), height, number);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
auto int age = 10;
static int weight = 125;
void(*myblock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &weight));
((void (*)(__block_impl *))((__block_impl *)myblock)->FuncPtr)((__block_impl *)myblock);
}
return 0;
}
可以看到__main_block_func_0并沒有添加任何全局變量,而是直接使用。這是因?yàn)槿肿兞繒?huì)一直存放在內(nèi)存中,全局都可以使用。
3. Block 的類型
既然 block 也是 OC 對(duì)象,那么 block 是什么類型呢?
聲明一個(gè) block,并打印其父類,如下所示:
void(^myblock)(void) = ^{
NSLog(@"github.com/pro648");
};
NSLog(@"%@", [myblock class]);
NSLog(@"%@", [[myblock class] superclass]);
NSLog(@"%@", [[[myblock class] superclass] superclass]);
NSLog(@"%@", [[[[myblock class] superclass] superclass] superclass]);
輸出如下:
__NSGlobalBlock__
NSBlock
NSObject
(null)
即 block 的繼承關(guān)系是:__NSGlobalBlock__ : NSBlock : NSObject。進(jìn)一步證實(shí)了 block 本質(zhì)上也是一個(gè) OC 對(duì)象。
定義三個(gè)不同的 block,分別打印其類型:
// 沒有調(diào)用外部變量的block
void(^myblock1)(void) = ^{
NSLog(@"github.com/pro648");
};
// 訪問auto變量
int age = 10;
void(^myblock2)(void) = ^{
NSLog(@"age: %d", age);
};
// 直接調(diào)用block的 class
NSLog(@"%@ %@ %@", [myblock1 class], [myblock2 class], [^{
NSLog(@"%d", age);
} class]);
打印如下:
__NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__
將上述代碼轉(zhuǎn)換為C++,可以看到三個(gè) block 類型都是_NSConcreteStackBlock類型。這可能是 runtime 運(yùn)行時(shí)進(jìn)行了某種轉(zhuǎn)換,使用 clang 生成的C++代碼僅供參考,不能保證和運(yùn)行時(shí)完全一致。
三種類型的block在內(nèi)存中的位置如下:

__NSGlobalBlock__、__NSStackBlock__、__NSMallocBlock__三種類型的block是按照以下規(guī)則產(chǎn)生的:
| block 類型 | 環(huán)境 |
|---|---|
__NSGlobalBlock__ |
沒有訪問auto變量 |
__NSStackBlock__ |
訪問了auto變量 |
__NSMallocBlock__ |
__NSStackBlock__調(diào)用了copy方法 |
3.1 __NSGlobalBlock__
當(dāng) block 內(nèi)沒有訪問auto變量時(shí),block 為__NSGlobalBlock__類型,__NSGlobalBlock__存在數(shù)據(jù)段中,程序結(jié)束才會(huì)回收內(nèi)存。但因?yàn)槠渑c普通函數(shù)沒有區(qū)別,很少使用__NSGlobalBlock__類型的 block。
3.2 __NSStackBlock__
在 block 內(nèi)訪問了auto變量為__NSStackBlock__類型。
__NSStackBlock__類型的 block 存放在棧中。棧的內(nèi)存由系統(tǒng)自動(dòng)分配和釋放,超出變量作用域后自動(dòng)釋放。由于棧中代碼超出作用域之后,內(nèi)存就會(huì)被銷毀,而有可能內(nèi)存銷毀之后才去調(diào)用它,此時(shí)就會(huì)出現(xiàn)問題。
ARC 自動(dòng)管理內(nèi)存時(shí)會(huì)幫助我們做很多事情,為了方便理解其本質(zhì),先關(guān)閉 ARC 使用 MRC 管理內(nèi)存。進(jìn)入TARGETS > Build Settings > Objective-C Automatic Reference Counting,修改其值為 NO。
關(guān)閉 ARC 后,使用以下代碼驗(yàn)證問題:
void (^myblock)(void);
void test() {
// __NSStackBlock__
int age = 10;
myblock = ^{
NSLog(@"age: %d", age);
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
myblock();
}
return 0;
}
執(zhí)行后控制臺(tái)輸出如下:
age: -272632840
這是因?yàn)閙yblock是在棧中的,即__NSStackBlock__類型的。當(dāng)test函數(shù)執(zhí)行完畢后,棧內(nèi)存中 block 已經(jīng)被系統(tǒng)回收。
3.3 __NSMallocBlock__
為了避免函數(shù)執(zhí)行完畢棧內(nèi)存立即被回收,可以將__NSStackBlock__block copy 到堆中。以下是修改后的代碼:
void (^myblock)(void);
void test() {
// __NSMallocBlock__
int age = 10;
// 將 block 從棧中復(fù)制到堆中。
myblock = [^{
NSLog(@"age: %d", age);
} copy];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
myblock();
}
return 0;
}
執(zhí)行后控制臺(tái)輸出如下:
age: 10
block 調(diào)用 copy 后,類型改變?nèi)缦滤荆?/p>
| block類型 | 內(nèi)存區(qū)域 | 調(diào)用copy的效果 |
|---|---|---|
__NSGlobalBlock__ |
數(shù)據(jù)段 | 什么都不做,類型不變。 |
__NSStackBlock__ |
棧 | 從棧復(fù)制到堆,類型變?yōu)?code>__NSMallocBlock__ |
__NSMallocBlock__ |
堆 | 引用計(jì)數(shù)加一,類型不變。 |
使用 MRC 管理內(nèi)存時(shí),經(jīng)常需要使用 copy 保存 block,將棧上的 block 復(fù)制到堆上,超出作用域時(shí) block 不會(huì)被釋放,后續(xù)需調(diào)用 release 銷毀 block。ARC 環(huán)境下,系統(tǒng)會(huì)自動(dòng)調(diào)用 copy 操作,使 block 不被銷毀;不再使用時(shí),自動(dòng)調(diào)用 release 引用計(jì)數(shù)減一。
4. ARC 在某些情況下會(huì)對(duì) block 自動(dòng)進(jìn)行一次 copy 操作,將其從棧區(qū)移動(dòng)到堆區(qū)
出現(xiàn)以下情況時(shí),ARC 會(huì)自動(dòng)對(duì) block 執(zhí)行一次 copy 操作,將其從棧區(qū)移動(dòng)到堆區(qū):
- 當(dāng) block 作為函數(shù)返回值時(shí)。
- 當(dāng) block 被強(qiáng)指針引用時(shí)。
- 當(dāng) Cocoa API 方法名包含usingBlock,且 block 作為參數(shù)時(shí),或 block 作為 GCD API 方法參數(shù)。
4.1 當(dāng) block 作為函數(shù)返回值時(shí)
typedef void (^MyBlock)(void);
MyBlock test() {
int age = 10;
// myblock 作為函數(shù)返回值,ARC 會(huì)自動(dòng)進(jìn)行copy。
MyBlock myblock = ^{
NSLog(@"age: %d", age);
};
return myblock;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyBlock myblock = test();
NSLog(@"%@", [myblock class]);
}
return 0;
}
在 ARC 環(huán)境下,參數(shù)返回值為 block 類型時(shí),系統(tǒng)會(huì)對(duì) ARC 自動(dòng)執(zhí)行一次 copy 操作,使其變?yōu)?code>__NSMallocBlock__類型。在 MRC 環(huán)境下,超出作用域后 block 會(huì)被銷毀,此時(shí)再調(diào)用會(huì)引起閃退。
4.2 當(dāng) block 被強(qiáng)指針引用時(shí)
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
MyBlock myblock = ^{
NSLog(@"age: %d", age);
};
NSLog(@"%@",[myblock class]);
}
return 0;
}
由于 block 訪問了auto變量,其是__NSStackBlock__類型。在 MRC 環(huán)境中,不會(huì)自動(dòng)進(jìn)行 copy 操作,輸出是__NSStackBlock__;在 ARC 環(huán)境中,有強(qiáng)指針引用時(shí)會(huì)自動(dòng)執(zhí)行 copy 操作,將 block 從棧中移動(dòng)到堆中。
修改上述代碼如下,即取消強(qiáng)指針對(duì) block 的引用:
int age = 10;
// 取消強(qiáng)指針的引用
NSLog(@"%@",[^{
NSLog(@"age: %d", age);
} class]);
可以看到輸出為:
__NSStackBlock__
手動(dòng)調(diào)用 copy,如下所示:
int age = 10;
NSLog(@"%@", [[^{
NSLog(@"age: %d", age);
} copy] class]);
輸出為:
__NSMallocBlock__
這也進(jìn)一步證明了 ARC 環(huán)境下,有強(qiáng)指針引用 block 時(shí)會(huì)自動(dòng)調(diào)用 copy 方法。
4.3 當(dāng) Cocoa API 方法名包含usingBlock,且 block 作為參數(shù)時(shí),或 block 作為 GCD API 的方法參數(shù)
當(dāng) Cocoa API 方法名包含usingBlock,且 block 作為參數(shù)時(shí),或 block 作為 GCD API 的方法參數(shù)。ARC 會(huì)根據(jù)情況自動(dòng)將棧上的 block copy到堆上。
// Cocoa API
NSArray *arr = @[@1];
[arr enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
// 這個(gè) block 在堆上
}];
// GCD API
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 這個(gè) block 在堆上
});
block 作為屬性時(shí)與其它屬性類似,但 MRC 環(huán)境下,只能使用copy修飾。因?yàn)?,block 訪問auto變量時(shí),block 是__NSStackBlock__類型,超出作用域 block 會(huì)被自動(dòng)銷毀。如果想要在外部繼續(xù)訪問、調(diào)用 block,就需要將 block 從棧中復(fù)制到堆中,因此需用copy修飾。
在 ARC 環(huán)境下,系統(tǒng)會(huì)在需要時(shí)自動(dòng)進(jìn)行 copy 操作。此時(shí)屬性可以使用strong,但copy更能表明用意。
5. Block 內(nèi)引用對(duì)象
之前 block 內(nèi)只引用過基本數(shù)據(jù)類型,這一部分介紹 block 內(nèi)引用對(duì)象類型。如下所示:
int main(int argc, const char * argv[]) {
@autoreleasepool {
{
Person *person = [[Person alloc] init];
person.age = 10;
^{
NSLog(@"person.age = %d", person.age);
}();
}
NSLog(@"--------");
}
return 0;
}
執(zhí)行后控制臺(tái)輸出如下:
person.age = 10
-[Person dealloc]
--------
可以看到在打印虛線前person已經(jīng)釋放。此時(shí),block 是棧類型 block,即__NSStackBlock__。棧區(qū) block 即便引用了對(duì)象,也會(huì)在超出作用域時(shí)一起釋放。
更新上述代碼如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyBlock myblock;
{
Person *person = [[Person alloc] init];
person.age = 10;
myblock = ^{
NSLog(@"person.age = %d", person.age);
};
myblock();
}
NSLog(@"--------");
}
return 0;
}
執(zhí)行后輸出如下:
person.age = 10
--------
-[Person dealloc]
可以看到執(zhí)行到虛線位置時(shí),person對(duì)象并沒有釋放。這是因?yàn)?block 內(nèi)部對(duì)person對(duì)象進(jìn)行了強(qiáng)引用,block 又被 myblock 強(qiáng)指針引用,即 block 是堆類型。堆類型的 block 會(huì)對(duì)外部對(duì)象強(qiáng)引用。
使用以下命令生成 C++ 代碼,查看其底層實(shí)現(xiàn):
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.0.0 main.m
查看 C++ 代碼,block 定義如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__strong person;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__strong _person, int flags=0) : person(_person) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
__main_block_desc_0定義如下:
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};
與 block 內(nèi)引用基本數(shù)據(jù)類型相比,__main_block_desc_0內(nèi)增加了copy和dispose兩個(gè)參數(shù),用于管理對(duì)象內(nèi)存。
copy操作調(diào)用的是__main_block_copy_0,如下所示:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->person,
(void*)src->person,
3/*BLOCK_FIELD_IS_OBJECT*/);
}
最終調(diào)用_Block_object_assign函數(shù),_Block_object_assign會(huì)對(duì)引用的對(duì)象person進(jìn)行引用計(jì)數(shù)操作。如果引用的對(duì)象是__strong修飾(默認(rèn)是__strong,即忽略時(shí)就是__strong),則引用計(jì)數(shù)加一;如果使用的__weak修飾,則引用計(jì)數(shù)不變。
當(dāng) block 執(zhí)行完畢,會(huì)調(diào)用dispose方法,dispose底層會(huì)調(diào)用以下方法:
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->person,
3/*BLOCK_FIELD_IS_OBJECT*/);
}
__main_block_dispose_0內(nèi)部會(huì)調(diào)用_Block_object_dispose方法。如果之前 copy 時(shí)使用了強(qiáng)引用,此時(shí)引用計(jì)數(shù)減一;如果之前使用了弱引用,直接取消對(duì)原來對(duì)象的弱引用。
6. Block 內(nèi)修改外部變量
如果外部變量是auto類型,block 通過值傳遞的方式捕獲變量。由于是值傳遞的方式進(jìn)行的,其不能修改外部變量。如果需要外部變量,可以通過以下兩種方式:
- 使用 static 修飾外部變量。
- 使用
__block修飾外部變量。
6.1 使用 static 修飾外部變量
使用 static 修飾的變量會(huì)一直存在內(nèi)存中,程序結(jié)束前不會(huì)被釋放。block 捕獲時(shí)通過引用方式進(jìn)行,即傳遞地址。因此,使用 static 修飾的外部變量可以直接修改值。
6.2 使用__block修飾外部變量
使用 static 修飾的變量會(huì)一直存放在內(nèi)存中,直到程序結(jié)束,這不利于性能優(yōu)化。
使用__block修飾外部變量,也可以達(dá)到在 block 內(nèi)修改成員變量的目的,那__block底層是如何實(shí)現(xiàn)的呢?
__block不能修飾全局變量、靜態(tài)變量。
下面代碼使用__block修飾局部變量:
__block int age = 10;
MyBlock myblock = ^{
age = 20;
NSLog(@"age: %d", age);
};
myblock();
使用以下命令將其轉(zhuǎn)換為 C++:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.0.0 main.m
轉(zhuǎn)換后的 block 定義如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// age 被封裝成了對(duì)象。
__Block_byref_age_0 *age; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到使用__block修飾的外部變量被封裝成了__Block_byref_age_0對(duì)象類型,__Block_byref_age_0聲明如下:
struct __Block_byref_age_0 {
// 也有isa指針,即也是對(duì)象類型。
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
// 值
int age;
};
__Block_byref_age_0結(jié)構(gòu)體也有isa指針,即也是對(duì)象類型。
使用__block修飾的age被轉(zhuǎn)換為:
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {
(void*)0,(__Block_byref_age_0 *)&age,
0,
sizeof(__Block_byref_age_0),
10
};
__main_block_func_0函數(shù)被轉(zhuǎn)換為:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // bound by ref
// 使用age的forwarding指向age。
(age->__forwarding->age) = 20;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_05_pj1lwvjs50j3gx6vjtvxcvf80000gn_T_main_d88942_mi_0, (age->__forwarding->age));
}
使用age的__forwarding取出變量地址,這樣即使 block 從棧移動(dòng)到了堆上,也可以正確修改變量值。
7. 對(duì)象類型的auto變量、__block變量
在 block 內(nèi)訪問了使用auto、__block修飾的對(duì)象類型的變量:
如果 block 在棧上,將不會(huì)對(duì)變量產(chǎn)生強(qiáng)引用。
-
如果 block 被拷貝到堆上
會(huì)調(diào)用 block 內(nèi)部的copy函數(shù)。
copy函數(shù)內(nèi)部會(huì)調(diào)用
_Block_object_assign函數(shù)。-
_Block_object_assign函數(shù)會(huì)根據(jù)變量修飾符__strong、__weak、__unsafe_unretained做出相應(yīng)操作,類似于 retain(形成強(qiáng)引用、弱引用)。使用
__block修飾的變量只有在 ARC 環(huán)境中會(huì)根據(jù)__strong、__weak、__unsafe_unretained修飾符進(jìn)行強(qiáng)引用,在 MRC 環(huán)境中不會(huì)進(jìn)行強(qiáng)引用。
-
如果 block 從堆上移除:
- 會(huì)調(diào)用 block 內(nèi)部的 dispose 函數(shù)。
- dispose 函數(shù)內(nèi)部會(huì)調(diào)用
_Block_object_dispose函數(shù)。 -
_Block_object_dispose函數(shù)會(huì)自動(dòng)釋放引用的變量,類似于 release。
8. Block 的循環(huán)引用
使用 block 容易產(chǎn)生循環(huán)引用。如果類中定義了一個(gè) block,在 block 內(nèi)又訪問了類的屬性,就會(huì)導(dǎo)致循環(huán)引用。
Person類中聲明了屬性age和 myblock,main.m文件中為 block 賦值,如下所示:
typedef void(^MyBlock)(void);
@interface Person : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, copy) MyBlock myblock;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.age = 10;
person.myblock = ^{
NSLog(@"age: %d", 20);
};
}
NSLog(@"-------");
return 0;
}
執(zhí)行后輸出如下:
-[Person dealloc]
-------
可以看到person先釋放,后打印虛線。
更新myblock賦值語句如下:
person.myblock = ^{
NSLog(@"age: %d", person.age);
};
再次執(zhí)行后,控制臺(tái)只輸出了虛線,person類沒有被釋放。
因?yàn)?code>person強(qiáng)引用了myblock,此時(shí)myblock在堆上;myblock內(nèi)訪問了person對(duì)象,堆上的 block 會(huì)對(duì)對(duì)象進(jìn)行強(qiáng)引用。此時(shí)person強(qiáng)引用myblock,myblock強(qiáng)引用person,形成了循環(huán)引用。
不止訪問
person會(huì)產(chǎn)生循環(huán)引用,在person類里的 block 內(nèi)訪問成員變量也會(huì)產(chǎn)生循環(huán)引用,因?yàn)樵L問成員變量本質(zhì)上是在調(diào)用self->instance,即仍然訪問了self。此外,OC 方法轉(zhuǎn)換為 C 語言方法后,默認(rèn)帶有兩個(gè)參數(shù)。第一個(gè)是 id 類型的
self,第二個(gè)參數(shù)是SEL類型的_cmd,因此,平常訪問的self也是局部變量。void test(id self, SEL _cmd) { }
ARC 環(huán)境下有以下三種解決循環(huán)引用的方案:
- 使用
__weak修飾變量。 - 使用
__unsafe_unretained修飾變量。 - 使用
__block修飾變量,同時(shí)在 block 內(nèi)將變量設(shè)置為nil,最后確保調(diào)用 block。
下面詳細(xì)介紹解決循環(huán)引用的方案。
8.1 使用__weak修飾變量
使用__weak修飾變量,更新如下:
__weak typeof(person) weakPerson = person;
person.myblock = ^{
NSLog(@"age: %d", weakPerson.age);
};
將其轉(zhuǎn)換為 C++ 代碼,__main_block_impl_0函數(shù)如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 對(duì)捕獲的person進(jìn)行弱引用。
Person *__weak weakPerson;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
此時(shí)執(zhí)行后,超出person作用域,person就會(huì)釋放。
8.2 使用__unsafe_unretained修飾變量
使用__unsafe_unretained修飾變量也可以解決循環(huán)引用問題。
__unsafe_unretained與__weak區(qū)別在于:
-
__weak:不會(huì)產(chǎn)生強(qiáng)引用。指向的對(duì)象銷毀時(shí),會(huì)自動(dòng)讓指針置為nil。 -
__unsafe_unretained:不會(huì)產(chǎn)生強(qiáng)引用,但沒有__weak安全。指向?qū)ο箐N毀時(shí),指針存儲(chǔ)地址不變,但內(nèi)存已經(jīng)被回收,再次訪問時(shí)產(chǎn)生野指針錯(cuò)誤。
8.3 使用__block修飾變量,同時(shí)在 block 內(nèi)將變量設(shè)置為nil,最后確保調(diào)用 block
使用__block也可以解決循環(huán)引用問題:
// 1.添加__block修飾符
__block Person *person = [[Person alloc] init];
person.age = 10;
person.myblock = ^{
NSLog(@"age: %d", person.age);
// 2.置為nil
person = nil;
};
// 3.調(diào)用block()
person.myblock();
使用__block解決循環(huán)引用問題時(shí),上述三步缺一不可。其缺點(diǎn)就是必須調(diào)用 block,如果沒有調(diào)用 block,就無法在 block 執(zhí)行完畢后將person置為nil,就無法解決循環(huán)引用問題。
在 MRC 環(huán)境中,有以下兩種方案解決循環(huán)引用問題:
- 使用
__unsafe_unretained,MRC 不支持弱指針__weak。 - 直接使用
__block。在 MRC 環(huán)境中,__block結(jié)構(gòu)體不會(huì)對(duì)結(jié)構(gòu)體內(nèi)對(duì)象進(jìn)行強(qiáng)引用,不會(huì)產(chǎn)生循環(huán)引用。
參考資料:
- Cocoa blocks as strong pointers vs copy
- A look inside blocks: Episode 3 (Block_copy)
- How blocks are implemented (and the consequences)
- Objective-C Blocks Ins And Outs
歡迎更多指正:https://github.com/pro648/tips
本文地址:https://github.com/pro648/tips/blob/master/sources/Block的本質(zhì).md