block的变量捕获机制
先看几段代码:
执行下面的代码会输出什么?
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;
}
会输出 a = 10, b = 20
执行下面的代码会输出什么?
int main(int argc, const char * argv[]) {
int age = 10;
void (^block)(void) = ^{
NSLog(@"age = %d",age);
};
age = 20;
block();
return 0;
}
会输出age = 10,但是age明明已经重新赋值成20了,为什么执行block age的值还是10 呢?
我们将代码通过clang -rewrite-objc main.m
命令将文件转换为cpp格式的文件,可以看到block的底层结构,可以看到上面这两种block的底层结构有什么区别:
第一种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的底层结构中,多了一个int类型 名字为age的变量,为什么会多一个这样的变量呢?
因为block为了保证在其内部能够正常访问外部的变量,block有一个变量捕获机制 capture,在创建block的时候,age=10, age这个值已经存储到block内部了,所以即使age后来被重新赋值,运行block时打印结果依然是age = 10,第一种block内部没有访问外界的变量,所以它的底层结构不会发生变化
此时 我们把age改成一个静态变量,作用域不变,就像这样:
int main(int argc, const char * argv[]) {
static int age = 10;
void (^block)(void) = ^{
NSLog(@"age = %d",age);
};
age = 20;
block();
return 0;
}
运行程序后会看到此时的打印结果为 age = 20,block运行后得出的值会随着age的改变而改变, 那么block是不是就没有捕获这个静态变量呢?
我们同样可以看一下这个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的底层结构中,依然会增加一个 *age 的变量,说明这种情况下block依然捕获了静态类型的age变量,与第二种block不同的是,第二种block相当于在block内部新建了一个int类型的变量来保存外部的那个age的值,而在这个block内部 相当于保存了外部age这个变量的内存地址,block内部的age与外部的age是同一个地址,所以当外部的age值改变时,block内部的age值也会改变
那如果age是一个全局变量 而不是一个局部变量呢?像这样:
int age = 10;//全局变量
static int height = 60;//静态全局变量
int main(int argc, const char * argv[]) {
void (^block)(void) = ^{
NSLog(@"age = %d, height = %d",age,height);
};
age = 20;
height = 120;
block();
return 0;
}
此时程序的运行结果为 age = 20,height = 120, 同样我们查看block的底层数据 发现 block并没有捕获这两个全局变量
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;
}
};
刚才用到的都是基础类型的变量,如果我们用对象类型的变量呢?来试一试:
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;
}
运行程序 得到的结果是:
屏幕快照 2019-06-16 上午9.49.34.png
可以看到 只有number3 的值和内存地址都发生了变化,其余的都没有变化,那其他三个是不是都没有被block捕获呢?我们还是通过这个block的底层结构来看一下:
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捕获的,因为NSNumber是对象类型本身就是一个指针,所以number3 是被block捕获了,从前后两次打印出来的number3的数据可以看出来两个number3的地址是不同的,block内部相当于新建了一个NSNumber类型的变量来保存外部的number3,而number4 在block内部是一个双指针,也就是block内部保存了这个number4内存地址的指针,所以两个number4前后的值和地址是一样的。
可以看到无论是基础类型或者对象类型,block对于变量的捕获机制基本是相同的:
- 局部变量
- auto变量 会被捕获,访问方式是值传递 (block内部会专门新增一个成员来存储auto变量的值,block运行时会访问这个新增的成员)
- static变量 会被捕获,访问方式是指针传递(问题:这种方式到底算不算捕获?)
- 全局变量 不会捕获,会直接去访问
可以看到只要是在block内部访问局部变量,那么block就会捕获这个变量,区别在于如果是自动变量是捕获它的值,而静态变量是捕获它的指针,如果block内部访问的是全局变量,block就不会捕获这个变量(无论是静态还是非静态全局变量)
那么block为什么要采用这种做法呢?为什么局部变量就需要捕获,全局变量就不用?
我们来看另一段代码:
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()
方法执行完毕之后,它方法内部的变量age和height就出了作用域了,在作用域之外就无法访问,然后执行block()
方法,而block()
方法内部又用到了age和height,但是此时这两个变量已经不能访问了(auto变量已经销毁,自然无法访问,static局部变量虽然不会销毁,但已经出了作用域,也不能访问),如果要保证正常的访问,就相当于要达到跨函数访问变量这种效果,所以block就会采用捕获局部变量这种方式来保证程序正常运行。
为什么auto变量是值传递?static变量是指针传递?
因为auto类型的局部变量 出了自己的作用域就被销毁了,这个变量就不存在了,它原来所占的内存就变成了垃圾内存了,不可以再访问,所以针对这种变量就需要在创建block的时候马上保存到block内部,否则在运行block的时候这个变量就可能没了,所以在block创建之后再怎么改变这个变量的值,运行block的时候依然是之前的值 。
而static局部变量虽然出了作用域也不能访问,但它的内存是一直存在的,不会销毁,所以block只需要在运行的时候能访问到它就可以,所以针对这种变量block采用的是指针传递,block内部只要保存这个变量的内存地址就可以保证在block运行的时候访问到这个变量,而正因为是指针传递,多以block在运行的时候总能够访问到这个变量最新的值。
看到这里,我们也很容易明白为什么全局变量不用捕获,因为全局变量既不会被销毁,也可以随处访问,所以block根本不用去捕获它也可能随时随地访问到它的值。
注意:
在一个类中的block的实现中用到了self,那这个block会捕获self(其实也就是这个类的实例对象),因为self是一个局部变量,通过类中的方法底层实现可以看到,每个方法的前两个参数都是self和方法名,那么self也就是一个参数,肯定是一个局部变量
如果block的实现中用到了这个类的某个属性(比如_name)那block也是会捕获的,因为_name相当于self->name,此时block会直接捕获self,而不是单单捕获name这个属性,同样对于self.name这样的属性,block也会捕获self
网友评论