當(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
通過(guò)轉(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就是通過(guò)myblock找到FuncPtr
指針,然后進(jìn)行調(diào)用洛姑。
myblock是指向__main_block_impl_0
結(jié)構(gòu)體的指針上沐,內(nèi)部并沒有FuncPtr
指針,為什么這里可以直接訪問(wèn)楞艾?這是因?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)訪問(wèn)auto
變量的方式是值傳遞恨狈,即直接將auto
變量傳遞給__main_block_impl_0
函數(shù)疏哗。
2.1.2 static變量
static
變量會(huì)一直存儲(chǔ)在內(nèi)存中。block 會(huì)捕獲static
修飾的局部變量,訪問(wèn)時(shí)使用指針訪問(wèn)返奉。
下面分別添加使用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");
};
// 訪問(wèn)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__ |
沒有訪問(wèn)auto變量 |
__NSStackBlock__ |
訪問(wèn)了auto變量 |
__NSMallocBlock__ |
__NSStackBlock__ 調(diào)用了copy方法 |
3.1 __NSGlobalBlock__
當(dāng) block 內(nèi)沒有訪問(wèn)auto
變量時(shí),block 為__NSGlobalBlock__
類型斗这,__NSGlobalBlock__
存在數(shù)據(jù)段中槽片,程序結(jié)束才會(huì)回收內(nèi)存感论。但因?yàn)槠渑c普通函數(shù)沒有區(qū)別拥褂,很少使用__NSGlobalBlock__
類型的 block镇匀。
3.2 __NSStackBlock__
在 block 內(nèi)訪問(wèn)了auto
變量為__NSStackBlock__
類型。
__NSStackBlock__
類型的 block 存放在棧中。棧的內(nèi)存由系統(tǒng)自動(dòng)分配和釋放彼水,超出變量作用域后自動(dòng)釋放崔拥。由于棧中代碼超出作用域之后,內(nèi)存就會(huì)被銷毀凤覆,而有可能內(nèi)存銷毀之后才去調(diào)用它链瓦,此時(shí)就會(huì)出現(xiàn)問(wè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)證問(wè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 訪問(wèn)了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)榈饷悖琤lock 訪問(wèn)auto
變量時(shí),block 是__NSStackBlock__
類型桩卵,超出作用域 block 會(huì)被自動(dòng)銷毀验靡。如果想要在外部繼續(xù)訪問(wèn)倍宾、調(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)只引用過(guò)基本數(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ì)原來(lái)對(duì)象的弱引用寺董。
6. Block 內(nèi)修改外部變量
如果外部變量是auto
類型覆糟,block 通過(guò)值傳遞的方式捕獲變量。由于是值傳遞的方式進(jìn)行的遮咖,其不能修改外部變量滩字。如果需要外部變量,可以通過(guò)以下兩種方式:
- 使用 static 修飾外部變量御吞。
- 使用
__block
修飾外部變量麦箍。
6.1 使用 static 修飾外部變量
使用 static 修飾的變量會(huì)一直存在內(nèi)存中,程序結(jié)束前不會(huì)被釋放魄藕。block 捕獲時(shí)通過(guò)引用方式進(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)訪問(wèn)了使用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)又訪問(wèn)了類的屬性杏头,就會(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
賦值語(yǔ)句如下:
person.myblock = ^{
NSLog(@"age: %d", person.age);
};
再次執(zhí)行后寓娩,控制臺(tái)只輸出了虛線,person
類沒有被釋放呼渣。
因?yàn)?code>person強(qiáng)引用了myblock
棘伴,此時(shí)myblock
在堆上;myblock
內(nèi)訪問(wèn)了person
對(duì)象屁置,堆上的 block 會(huì)對(duì)對(duì)象進(jìn)行強(qiáng)引用焊夸。此時(shí)person
強(qiáng)引用myblock
,myblock
強(qiáng)引用person
缰犁,形成了循環(huán)引用淳地。
不止訪問(wèn)
person
會(huì)產(chǎn)生循環(huán)引用怖糊,在person
類里的 block 內(nèi)訪問(wèn)成員變量也會(huì)產(chǎn)生循環(huán)引用帅容,因?yàn)樵L問(wèn)成員變量本質(zhì)上是在調(diào)用self->instance
,即仍然訪問(wèn)了self
伍伤。此外并徘,OC 方法轉(zhuǎn)換為 C 語(yǔ)言方法后,默認(rèn)帶有兩個(gè)參數(shù)扰魂。第一個(gè)是 id 類型的
self
麦乞,第二個(gè)參數(shù)是SEL
類型的_cmd
蕴茴,因此,平常訪問(wèn)的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)引用問(wè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)被回收解恰,再次訪問(wèn)時(shí)產(chǎn)生野指針錯(cuò)誤锋八。
8.3 使用__block
修飾變量,同時(shí)在 block 內(nèi)將變量設(shè)置為nil护盈,最后確保調(diào)用 block
使用__block
也可以解決循環(huán)引用問(wè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)引用問(wèn)題時(shí)挟纱,上述三步缺一不可。其缺點(diǎn)就是必須調(diào)用 block腐宋,如果沒有調(diào)用 block紊服,就無(wú)法在 block 執(zhí)行完畢后將person
置為nil,就無(wú)法解決循環(huán)引用問(wèn)題胸竞。
在 MRC 環(huán)境中欺嗤,有以下兩種方案解決循環(huán)引用問(wè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