block的變量捕獲機(jī)制
先看幾段代碼:
執(zhí)行下面的代碼會(huì)輸出什么?
int main(int argc, const char * argv[]) {
void(^block)(int,int) = ^(int a, int b){
NSLog(@"a = %d, b = %d",a,b);
};
block(10,20);
return 0;
}
會(huì)輸出 a = 10, b = 20
執(zhí)行下面的代碼會(huì)輸出什么?
int main(int argc, const char * argv[]) {
int age = 10;
void (^block)(void) = ^{
NSLog(@"age = %d",age);
};
age = 20;
block();
return 0;
}
會(huì)輸出age = 10,但是age明明已經(jīng)重新賦值成20了,為什么執(zhí)行block age的值還是10 呢?
我們將代碼通過(guò)clang -rewrite-objc main.m命令將文件轉(zhuǎn)換為cpp格式的文件,可以看到block的底層結(jié)構(gòu),可以看到上面這兩種block的底層結(jié)構(gòu)有什么區(qū)別:
第一種block:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
第二種block
struct __main_block_impl_0 {
struct __block_impl impl;
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;
}
};
可以看到第二種block的底層結(jié)構(gòu)中,多了一個(gè)int類型 名字為age的變量,為什么會(huì)多一個(gè)這樣的變量呢?
因?yàn)閎lock為了保證在其內(nèi)部能夠正常訪問(wèn)外部的變量,block有一個(gè)變量捕獲機(jī)制 capture,在創(chuàng)建block的時(shí)候,age=10, age這個(gè)值已經(jīng)存儲(chǔ)到block內(nèi)部了,所以即使age后來(lái)被重新賦值,運(yùn)行block時(shí)打印結(jié)果依然是age = 10,第一種block內(nèi)部沒有訪問(wèn)外界的變量,所以它的底層結(jié)構(gòu)不會(huì)發(fā)生變化
此時(shí) 我們把a(bǔ)ge改成一個(gè)靜態(tài)變量,作用域不變,就像這樣:
int main(int argc, const char * argv[]) {
static int age = 10;
void (^block)(void) = ^{
NSLog(@"age = %d",age);
};
age = 20;
block();
return 0;
}
運(yùn)行程序后會(huì)看到此時(shí)的打印結(jié)果為 age = 20,block運(yùn)行后得出的值會(huì)隨著age的改變而改變, 那么block是不是就沒有捕獲這個(gè)靜態(tài)變量呢?
我們同樣可以看一下這個(gè)block的底層結(jié)構(gòu):
struct __main_block_impl_0 {
struct __block_impl impl;
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;
}
};
可以看到block的底層結(jié)構(gòu)中,依然會(huì)增加一個(gè) *age 的變量,說(shuō)明這種情況下block依然捕獲了靜態(tài)類型的age變量,與第二種block不同的是,第二種block相當(dāng)于在block內(nèi)部新建了一個(gè)int類型的變量來(lái)保存外部的那個(gè)age的值,而在這個(gè)block內(nèi)部 相當(dāng)于保存了外部age這個(gè)變量的內(nèi)存地址,block內(nèi)部的age與外部的age是同一個(gè)地址,所以當(dāng)外部的age值改變時(shí),block內(nèi)部的age值也會(huì)改變
那如果age是一個(gè)全局變量 而不是一個(gè)局部變量呢?像這樣:
int age = 10;//全局變量
static int height = 60;//靜態(tài)全局變量
int main(int argc, const char * argv[]) {
void (^block)(void) = ^{
NSLog(@"age = %d, height = %d",age,height);
};
age = 20;
height = 120;
block();
return 0;
}
此時(shí)程序的運(yùn)行結(jié)果為 age = 20,height = 120, 同樣我們查看block的底層數(shù)據(jù) 發(fā)現(xiàn) block并沒有捕獲這兩個(gè)全局變量
int age = 10;
static int height = 60;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
剛才用到的都是基礎(chǔ)類型的變量,如果我們用對(duì)象類型的變量呢?來(lái)試一試:
NSNumber *number1;
static NSNumber *number2;
int main(int argc, const char * argv[]) {
NSNumber *number3;
static NSNumber *number4;
number1 = @1;
number2 = @2;
number3 = @3;
number4 = @4;
void (^block)(void) = ^{
NSLog(@"number1 = %@ number1Pointer = %p",number1,&number1);
NSLog(@"number2 = %@ number2Pointer = %p",number2,&number2);
NSLog(@"number3 = %@ number3Pointer = %p",number3,&number3);
NSLog(@"number4 = %@ number4Pointer = %p",number4,&number4);
};
number1 = @10;
number2 = @20;
number3 = @30;
number4 = @40;
NSLog(@"number1 = %@ number1Pointer = %p",number1,&number1);
NSLog(@"number2 = %@ number2Pointer = %p",number2,&number2);
NSLog(@"number3 = %@ number3Pointer = %p",number3,&number3);
NSLog(@"number4 = %@ number4Pointer = %p",number4,&number4);
block();
return 0;
}
運(yùn)行程序 得到的結(jié)果是:

可以看到 只有number3 的值和內(nèi)存地址都發(fā)生了變化,其余的都沒有變化,那其他三個(gè)是不是都沒有被block捕獲呢?我們還是通過(guò)這個(gè)block的底層結(jié)構(gòu)來(lái)看一下:
NSNumber *number1;
static NSNumber *number2;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSNumber *number3;
NSNumber **number4;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSNumber *_number3, NSNumber **_number4, int flags=0) : number3(_number3), number4(_number4) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
首先可以確定的是 number1和number2是沒有被block捕獲的,因?yàn)镹SNumber是對(duì)象類型本身就是一個(gè)指針,所以number3 是被block捕獲了,從前后兩次打印出來(lái)的number3的數(shù)據(jù)可以看出來(lái)兩個(gè)number3的地址是不同的,block內(nèi)部相當(dāng)于新建了一個(gè)NSNumber類型的變量來(lái)保存外部的number3,而number4 在block內(nèi)部是一個(gè)雙指針,也就是block內(nèi)部保存了這個(gè)number4內(nèi)存地址的指針,所以兩個(gè)number4前后的值和地址是一樣的。
可以看到無(wú)論是基礎(chǔ)類型或者對(duì)象類型,block對(duì)于變量的捕獲機(jī)制基本是相同的:
- 局部變量
- auto變量 會(huì)被捕獲,訪問(wèn)方式是值傳遞 (block內(nèi)部會(huì)專門新增一個(gè)成員來(lái)存儲(chǔ)auto變量的值,block運(yùn)行時(shí)會(huì)訪問(wèn)這個(gè)新增的成員)
- static變量 會(huì)被捕獲,訪問(wèn)方式是指針傳遞(問(wèn)題:這種方式到底算不算捕獲?)
- 全局變量 不會(huì)捕獲,會(huì)直接去訪問(wèn)
可以看到只要是在block內(nèi)部訪問(wèn)局部變量,那么block就會(huì)捕獲這個(gè)變量,區(qū)別在于如果是自動(dòng)變量是捕獲它的值,而靜態(tài)變量是捕獲它的指針,如果block內(nèi)部訪問(wèn)的是全局變量,block就不會(huì)捕獲這個(gè)變量(無(wú)論是靜態(tài)還是非靜態(tài)全局變量)
那么block為什么要采用這種做法呢?為什么局部變量就需要捕獲,全局變量就不用?
我們來(lái)看另一段代碼:
void (^block)(void);
void test(){
int age = 10;
static int height = 60;
block = ^{
NSLog(@"age = %d, height = %d",age,height);
};
}
int main(int argc, const char * argv[]) {
test();
block();
return 0;
}
很明顯 test()方法執(zhí)行完畢之后,它方法內(nèi)部的變量age和height就出了作用域了,在作用域之外就無(wú)法訪問(wèn),然后執(zhí)行block()方法,而block()方法內(nèi)部又用到了age和height,但是此時(shí)這兩個(gè)變量已經(jīng)不能訪問(wèn)了(auto變量已經(jīng)銷毀,自然無(wú)法訪問(wèn),static局部變量雖然不會(huì)銷毀,但已經(jīng)出了作用域,也不能訪問(wèn)),如果要保證正常的訪問(wèn),就相當(dāng)于要達(dá)到跨函數(shù)訪問(wèn)變量這種效果,所以block就會(huì)采用捕獲局部變量這種方式來(lái)保證程序正常運(yùn)行。
為什么auto變量是值傳遞?static變量是指針傳遞?
因?yàn)閍uto類型的局部變量 出了自己的作用域就被銷毀了,這個(gè)變量就不存在了,它原來(lái)所占的內(nèi)存就變成了垃圾內(nèi)存了,不可以再訪問(wèn),所以針對(duì)這種變量就需要在創(chuàng)建block的時(shí)候馬上保存到block內(nèi)部,否則在運(yùn)行block的時(shí)候這個(gè)變量就可能沒了,所以在block創(chuàng)建之后再怎么改變這個(gè)變量的值,運(yùn)行block的時(shí)候依然是之前的值 。
而static局部變量雖然出了作用域也不能訪問(wèn),但它的內(nèi)存是一直存在的,不會(huì)銷毀,所以block只需要在運(yùn)行的時(shí)候能訪問(wèn)到它就可以,所以針對(duì)這種變量block采用的是指針傳遞,block內(nèi)部只要保存這個(gè)變量的內(nèi)存地址就可以保證在block運(yùn)行的時(shí)候訪問(wèn)到這個(gè)變量,而正因?yàn)槭侵羔槀鬟f,多以block在運(yùn)行的時(shí)候總能夠訪問(wèn)到這個(gè)變量最新的值。
看到這里,我們也很容易明白為什么全局變量不用捕獲,因?yàn)槿肿兞考炔粫?huì)被銷毀,也可以隨處訪問(wèn),所以block根本不用去捕獲它也可能隨時(shí)隨地訪問(wèn)到它的值。
注意:
在一個(gè)類中的block的實(shí)現(xiàn)中用到了self,那這個(gè)block會(huì)捕獲self(其實(shí)也就是這個(gè)類的實(shí)例對(duì)象),因?yàn)閟elf是一個(gè)局部變量,通過(guò)類中的方法底層實(shí)現(xiàn)可以看到,每個(gè)方法的前兩個(gè)參數(shù)都是self和方法名,那么self也就是一個(gè)參數(shù),肯定是一個(gè)局部變量
如果block的實(shí)現(xiàn)中用到了這個(gè)類的某個(gè)屬性(比如_name)那block也是會(huì)捕獲的,因?yàn)開name相當(dāng)于self->name,此時(shí)block會(huì)直接捕獲self,而不是單單捕獲name這個(gè)屬性,同樣對(duì)于self.name這樣的屬性,block也會(huì)捕獲self