Block如何捕获外部变量一:基本数据类型
共有三种:auto变量 、static变量、全局变量
这一篇,我们详细讲解Block捕获外部变量的机制.我们把block捕获外部基本数据类型变量的情况分为以下几种,见下图:
一:auto变量
auto变量:自动变量,离开作用域就会销毁,一般我们创建的局部变量都是auto变量 ,比如 int age = 10
,系统会在默认在前面加上auto int age = 10
首先我们要搞清楚,什么是捕获,所谓捕获外部变量,意思就是在block内部,创建一个变量来存放外部变量,这就叫做捕获.先做一个小小的Test:
{ int age = 10; void (^block)(void) = ^{ NSLog(@"age is %d",age); }; age = 20; block(); }
输出的age
是10
定义一个age
变量,在block
内部访问这个age
,在调用block
之前,修改这个age
值,那么最后输出的age
是多少呢?很简单,输出的age
还是10,相信很多人都知道结果,我们从底层来看一下为什么会这样.
上面代码通过Clang编译器转换后如下:
{ int age = 10; //定义block void (*block)(void) = &__main_block_impl_0( __main_block_func_0, &__main_block_desc_0_DATA, age ); age = 20; //调用block block->FuncPtr(block); return 0; }
我们看到在调用block的构造函数时,传入了三个参数,分别是:__main_block_func_0
,&__main_block_desc_0_DATA
,age
,
我们找到block
的构造函数,看看内部如何处理这个age
:
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int age; // 1: 定义了一个同名的age变量 //block构造函数 __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; age = _age; //2 :C++的特殊语法,在构造函数内部会默认把_age赋值给age } };
通过查看block
的内部结构看我们发现,block
内部创建了一个age
变量,并且在block
构造函数中,把传递进来的_age
赋值给了这个age
变量.我们看看调用block
时,他的底部取的是哪个age
:
//调用block的FuncPtr函数,把block当做参数传递进去 block->FuncPtr(block); //FuncPtr函数内部 static void __main_block_func_0(struct __main_block_impl_0 *__cself) { //通过传递的block,找到block内部的age int age = __cself->age; // bound by copy //打印age NSLog((NSString *)&__NSConstantStringImpl__var_folders_5t_pxd6sp5x6rl9gnk21q2q934h0000gn_T_main_3089d7_mi_0,age); }
通过底层代码,我们看到,在调用block
时,block
会找到自己内部的age
变量, 然后打印数出,所以我们修改age = 20
, 并不会影响block
内部的age
值
二:static变量
我们把上面的代码稍作修改:
auto int age = 10; static int height = 20; void (^block)(void) = ^{ NSLog(@"age is %d, height is %d",age,height); }; age = 20; height = 30; block();
打印的结果是age is 10, height is 30
同样转换 C++ 代码,查看底层:
{ auto int age = 10; static int height = 20; void (*block)(void) = &__main_block_impl_0( __main_block_func_0, &__main_block_desc_0_DATA, age, &height //传递指针 ); age = 20; height = 30; (block->FuncPtr(block); }
我们看到,在定义block
时,调用block
的构造函数,传递参数时,age
传递的是值,而height
传递的是指针,看看构造函数内部:
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int age;//定义 age 变量 int *height;//定义一个 指针变量,存放外部变量的指针 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
我们看到,在block
内部,定义了两个变量age
,height
,不同的是,height
是一个指针指针变量
,用于存放外部变量的指针.我们再来看看执行block
代码块的内部:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int age = __cself->age; // bound by copy int *height = __cself->height; // bound by copy // *height : 取出指针变量所指向的内存的值 NSLog((NSString *)&__NSConstantStringImpl__var_folders_5t_pxd6sp5x6rl9gnk21q2q934h0000gn_T_main_bf6cae_mi_0,age,(*height)); }
我们看到,对于age
是捕获到内部,把外部age
的值存起来,而对于height
,是把外部变量的指针保存起来,所以, 我们在修改height
时,会影响到block
内部的值
思考:为什么会出现这两种情况 ?(block捕获外部基本数据类型变量: auto变量是值传递;static变量是指针传递)?
原因很简单,因为auto
是自动变量,出了作用域后会自动销毁的,如果我们保留他的指针,就会存在访问野指针的情况
//定义block类型 void(^block)(void); void test(){ int age = 10; static int height = 20; //在block内部访问 age , height block = ^{ NSLog(@"age is %d, height is %d",age,height); }; age = 20; height = 30; } //在main函数中调用 int main(int argc, const char * argv[]) { test(); //test调用后,age变量就会自动销毁,如果block内部是保留age变量的指针,那么我们在调用block()时,就出现访问野指针 block(); }
三:全局变量
全局变量哪里都可以访问,所以block
内部是不会捕获全局变量的,直接访问,这个很好理解,我们直接看代码:
全局变量底层
为什么全局变量不需要捕获?
因为全局变量无论哪个函数都可以访问,block
内部当然也可以正常访问,所以根本无需捕获
为什么局部变量就需要捕获呢?
因为作用域的问题,我们在一个函数中定义变量,在block
内部访问,本质上跨函数访问,所以需要捕获起来.
Test1:
我们在Person
类中写一个test()
方法,在test()
方法中定义一个block
并访问self
, 请问block
会不会捕获self
.
@implementation Person - (void)test{ void(^block)(void) = ^{ NSLog(@"会不会捕获self--%@",self); }; block(); } @end
答案是会捕获self
,我们看看底层代码:
struct __Person__test_block_impl_0 { struct __block_impl impl; struct __Person__test_block_desc_0* Desc; Person *self; __Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
很显然block
内部的确声明了一个Person *self
用于保存self
,既然block
内部捕获了self
,那就说明self
肯定是一个局部变量.那问题就来了,
为什么self
会是一个局部变量?它应该是一个全局变量呀?我们看一下转换后的test()
方法:
static void _I_Person_test(Person * self, SEL _cmd) { void(*block)(void) = ((void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344)); ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); }
我们 OC 中的test()
方法时没有参数的,但是转换成 C++ 后就多了两个参数self
,_cmd
,其实我们每个 OC 方法都会默认有这两个参数,这也是为什么我们在每个方法中都能访问self
和_cmd
,而参数就是局部变量,所以block
就自然而然的捕获了self
.
Test2:
我们对 Test1 稍加修改,增加一个name
属性,然后在block
中访问_name
,这时候block
会捕获self
吗?
答案是:会.继续看一下底层:
block
底层仍然捕获了self
,这是因为,我们去访问_name
属性的时候,实际上相当于self -> name
,要想获取name
,必须要先获取self
,因为name
是从self
中来的,所以block
内部会对self
进行捕获.
总结:
一:只要是局部变量,不管是auto 变量
,还是static 变量
,block
都会捕获. 不同的是,对于auto 变量
,block
是保存值,而static 变量
是保存的指针.
二:如果是全局变量,根本不需要捕获,直接访问
本篇只是讲解了block
捕获基本数据类型的auto
变量,下一篇会讲解block捕获对象类型的auto变量