block本质探寻二之变量捕获
一、代码
说明:本文章须结合文章《block本质探寻一之内存结构》和《class和object_getClass方法区别》加以理解;
//main.m
#import <Foundation/Foundation.h> int a = 10; static int b = 10; int main(int argc, const char * argv[]) { @autoreleasepool { auto int c = 20; static int d = 20; void (^block)(void) = ^{ NSLog(@"a=%d, b=%d, c=%d, d=%d", a, b, c, d); }; a = 30; b = 35; c = 40; d = 45; block(); } return 0; }
//打印
2019-01-09 15:42:16.246684+0800 MJ_TEST[4180:224738] a=30, b=35, c=20, d=45 Program ended with exit code: 0
分析:很显然,只有c的值没有改变,其它变量的值都改变了——为什么,看下底层代码实现;
二、main.cpp
int a = 10; static int b = 10; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int c; int *d; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _c, int *_d, int flags=0) : c(_c), d(_d) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int c = __cself->c; // bound by copy int *d = __cself->d; // bound by copy NSLog((NSString *)&__NSConstantStringImpl__var_folders_tb_zgsq5gq15rd3zvbdmw1c09y80000gn_T_main_1f1f41_mi_0, a, b, c, (*d)); } 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 c = 20; static int d = 20; void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, c, &d)); a = 30; b = 35; c = 40; d = 45; ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); } return 0; }
分析:
1)C语言语法
<1>int c被转换成auto int c:我们知道c、d为局部变量,而a、b为全局变量,C语言中,所有没有修饰符的局部变量默认的修饰符为auto,static修饰的变量为静态变量,还有一个register注册类的在此不再赘述(自己有兴趣上网查下);
<2>auto类型的局部变量的生命周期为离其最近的大括号内,超出该大括号,该变量被自动销毁;
<3>static类型的变量(不论是全局还是局部),其值一直保留在内存中,不受大括号的限制,程序结束时才被销毁;
2)变量捕获概念
我们发现在block结构体中,存在c、d而不存在a、b变量,在此,我们把存在于block结构体中的外部变量称为变量捕获(存在的形式在所不问),不存在的则没有被捕获,所以a、b变量没有被block捕获;
3)变量调用流程
<1>在block结构体中,c保持不变依然为int型变量,而d被转换成int型指针变量,因此在main函数中通过__main_block_impl_0方法传递实参c本身的值和d指向的内存的值&d;
而在block的构造函数中,c(_c), d(_d)为C++语法<=>c = _c,d = _d,那么main函数中的实参c、&d最终传递给了block结构体中的变量c和指针变量d;
<2>最后在__main_block_func_0方法中,对c、d而言,须先获取block内部的成员变量再输出;而对于a、b,因为是全局变量,所以可以直接引用;
综上所述:
auto局部变量因为作用域(或生命周期)有限,随时会销毁,故block在引用时系统会自动将其值保存在block结构体中(即捕获);而全局变量和static修饰的变量(局部或全局),并不会随时被销毁,其值一直会在内存中保持不变,知道整个程序结束时才销毁
1)另外从另一个角度理解,全局变量其作用域为从其定义的地方开始到该文件结束止都是有效的,所以main函数中可以用,__main_block_func_0函数中也可以用,不需要再将其保存到block自身的结构体中;
2)static修饰的局部变量会被转化成指针变量,而保存到block结构体中也是指针,因为指针本身的值为另一个变量的地址,所以block对该指针的操作始终是对另一个变量的地址的操作,而非另一个变量值的本身,当对d重新赋值时,block中的指针变量指向的变量的值也就随之改变,对*d输出当然被改变(*d即取出指向的内存地址存放的值);
3)auto局部变量被捕获,即是在内存中重新开辟了内存来存放该变量的值(即copy),只不过是在block结构体对应的内存中;
三、结论
1.
2.auto修饰的局部变量在block定义后的修改,不影响block内部对该变量的使用;后两者,有影响;
四、扩展——OC对象捕获问题
1)Person.m——注:此处我将.h文件也贴过来了,为了很好的阅读
#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface Person : NSObject @property (nonatomic, copy) NSString *name; - (instancetype)initWithName:(NSString *)name; @end NS_ASSUME_NONNULL_END #import "Person.h" int weight_ = 10; @implementation Person - (void)test { void(^block)(void) = ^{ NSLog(@"-----%p", self); NSLog(@"-----%@", _name); NSLog(@"-----%@", self.name); NSLog(@"-----%d", weight_); }; } - (instancetype)initWithName:(NSString *)name { self = [super init]; if (self) { } return self; } @end
2)Person.cpp——注:此处只对.m文件进行转化
问题一:参数
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)); } static instancetype _Nonnull _I_Person_initWithName_(Person * self, SEL _cmd, NSString * _Nonnull name) { self = ((Person *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Person"))}, sel_registerName("init")); if (self) { } return self; }
分析:
<1>在.m文件内部有两个方法test和initWithName,前者不带参数,后者带一个参数name,但是转成C++后,发现,两个方法前面均自动加上了两个参数:Person * self, SEL _cmd;这是每个方法必备的两个参数,前者是调用对象本身self,后者是方法名;
<2>此处的self为实例对象而非类对象(验证方法:在test方法中打印self的地址%p,会发现每次调用的值都不一样,而类对象在内存中只有一份;
问题二: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; } };
分析:
<1>self被捕获到block结构体体中,那么可以肯定self是auto类型的局部变量;
<2>从另一个角度理解:self作为实参从main函数中传递到block结构体的构造函数__Person__test_block_impl_0的形参_self,再将_slef赋值于self;而参数本身就是一个auto类型的局部变量,函数结束后就自动被销毁;
问题三:block代码块执行
int weight_ = 10; static void __Person__test_block_func_0(struct __Person__test_block_impl_0 *__cself) { Person *self = __cself->self; // bound by copy NSLog((NSString *)&__NSConstantStringImpl__var_folders_tb_zgsq5gq15rd3zvbdmw1c09y80000gn_T_Person_952ecd_mi_0, self); NSLog((NSString *)&__NSConstantStringImpl__var_folders_tb_zgsq5gq15rd3zvbdmw1c09y80000gn_T_Person_952ecd_mi_1, (*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_Person$_name))); NSLog((NSString *)&__NSConstantStringImpl__var_folders_tb_zgsq5gq15rd3zvbdmw1c09y80000gn_T_Person_952ecd_mi_2, ((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("name"))); NSLog((NSString *)&__NSConstantStringImpl__var_folders_tb_zgsq5gq15rd3zvbdmw1c09y80000gn_T_Person_952ecd_mi_3, weight_); }
分析:
<1>self作为auto类型的局部变量,输出前需先从block结构体中取出该成员变量;
<2>weight作为全局变量,直接引用,无须捕获;
<3>以_name来引用对象属性,其本质是block的成员变量,存放在类对象的结构体内存中,而结构体指针变量须通过"->"来引用该结构体成员变量,但是self作为实例对象与Person类对象不是同一个,问什么能通过"->"来引用?
————原因:self的第一个成员变量为isa,而isa是指向类对象的指针,即类对象的首地址跟实例对象的首地址是同一个地址,而结构体成员变量在内存中的地址是连续的,因此self可以通过"->"形式来找到_name成员变量;
<4>self.name即通过getter方法来访问name值,转换成C++为objc_msgSend方法,即通过消息转发机制来访问name值,而消息转发机制的本质是通过isa来找到类对象,进而访问该类对象中的name成员变量;
3)结论
【1】oc实例对象self会被捕获到block结构体中;
【2】@property声明的属性的引用,须先执行【1】步骤,因此严格上讲也是被捕获到block中;
【3】.m文件中声明的全局变量,不受self影响,依然不会被捕获到block中,;