上面一节讲解了block的本质,举例了block最简单结构的一种情况。如果更复杂了呢。比如block函数执行调用外部参数,会有哪些情况呢?不同的情况,他们又有什么异同点呢?这里先把分析结果写在最前面,不懂的可以先跳过,看下面的正文每一个情形有底层结构分析。如果能看明白的,可以不看下面的正文,说明你已经很清楚 block 变量捕获机制了:
1、block变量捕获机制(capture)
1、捕获(capture):是指在 block 内部会专门新增一个成员变量,来存储传进来的值。或者说将外面的值捕获到 block 结构体中存储。
2、{ ... } :函数作用域,内部声明的变量都是局部变量。
3、auto:又称自动变量。其作用是离开对应的作用域该变量就会被销毁(值传递)。
(c语言中,当定义出一个局部变量的时候,会默认添加,所以不再需要手动添加)
如下面两个定义的变量是等价的:
auto int age = 0;
int age = 0;
注意:auto只存在于局部变量中。
4、static:静态变量,又分静态局部变量,静态全局变量
2、局部变量
1、只要是局部变量而且是 block 要访问的局部变量,block 都会将这些局部变量捕获到block结构体中。
2、捕获的区别就是 auto 进行的值传递,而 static 进行的是指针传递。
3、局部变量为什么会有自动变量和局部静态变量的差异呢?
1、auto 局部变量的内存随时可能被销毁,内存会消失。所以优先保证变量的值能拿到。
2、static 地址一直存在,可以在需要的时候取值,所以这里优先保证取得的数据最新。
4、为什么局部变量需要捕获,而全局变量不需要捕获?
1、局部变量:因为 block 应用存在跨函数访问,那么局部变量就有可能被销毁。为了使执行block的时候有值,所以才想到将这些变量捕获到block的结构体中,然后访问结构体中的变量。
2、全局变量:只要是全局变量,block内部不需要去捕获这些变量,可以直接进行访问。
5、关于 self 局部变量
1、在OC中我们可以看到,当函数转换成C++时,都默认带有两个参数,第一个参数是调用者本身self,第二个参数是 _cmd 这个方法的方法名称。就是说,OC函数也是默认有这两个参数的,只是默认隐藏不显示出来而已。因此我们可以知道 self 是局部变量,不是全局变量, self 指向的是方法调用者本身。
2、正因为 self 是局部变量,所以 self 会捕获到 block 里面去。
1、block执行的时候传递参数、
1.1、代码示例
#import <Foundation/Foundation.h> int main(int argc, const char * argv[]) { @autoreleasepool { void(^block)(int,int) = ^(int a,int b){ NSLog(@"a:%d b:%d",a,b); }; block(10,10); } return 0; }
1.2、代码转成C++
打开命令行,cd 到 main.m 的目录下,执行以下命令:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
执行完命令后,在同目录下会生成 main.cpp 文件,然后将文件导入项目(注意:该文件不参与编译)。
代码转成c++,都是一样的命令,后面就不写了。
1.3、底层结构分析
// 1、程序入口 int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; // 2、定义 block 变量 void(*block)(int,int) = &__main_block_impl_0( __main_block_func_0, &__main_block_desc_0_DATA); // /** * 3、执行 block 变量 * 调用封装到block的函数方法,通过上一节我们可以直接看出来,这里传递了两个参数 */ block->FuncPtr(block, 10, 10); } return 0; } // 4、block内部封装了的函数方法,传递了两个参数,然后在方法内部直接引用。 static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) { NSLog((NSString *)&__NSConstantStringImpl__var_folders_vc_pn677_yj1sz8hgf_q5bjvssc0000gn_T_main_ed1930_mi_0,a,b); }
2、block内部引用局部变量
2.1、代码示例
#import <Foundation/Foundation.h> int main(int argc, const char * argv[]) { @autoreleasepool { int age = 10; void(^block)(void) = ^{ NSLog(@"age:%d",age); }; age = 20; block(); } return 0; }
2.2、底层结构分析
// 程序入口 int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; // tag-1、变量 int age = 10; // tag-2、定义函数:执行结构体函数的时候,这里多了一个 age 值传递 void(*block)(void) = &__main_block_impl_0( __main_block_func_0, &__main_block_desc_0_DATA, age// tag-2.1、值传递 ); // tag-3、因为block定义的时候,只是对参数age进行值传递,所以block结构体内部生成【tag-2.1标记】的age值是独立存在的。 // 那么这里修改变量的值,只是【tag-1标记】外边的值,影响不到block内部。 age = 20; // tag-4、执行block的函数,这里面取的age的值,是block结构体内部的age值 block->FuncPtr(block); } return 0; } // tag-2.1、block 结构体 struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int age; // tag-2.1、生成一个新的值 __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; } }; // tag-4、执行block的函数,这里面取的age的值,是block结构体内部的age值 static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int age = __cself->age; // bound by copy NSLog((NSString *)&__NSConstantStringImpl__var_folders_vc_pn677_yj1sz8hgf_q5bjvssc0000gn_T_main_73f634_mi_0,age); }
分析总结:
其逻辑结构图如下:
1、刚开始 age = 10 的值,传递给了block内部的age【值传递】,它们之间没有绑定关系。
2、外部修改了age的值为20。
3、但是block内部的age变量,跟外部的age没有形成绑定关系。
4、综上,代码块内部的函数方法,取得值还是block内部的age值,打印结果还是为10。
总结:
修改 block 外部成员变量的值,是不会影响到 block 内部成员变量的值。
3、block的变量捕获(capture)
那么问题就来了,怎么让 block 内部能够正常访问外部的变量。
这里就涉及到了block变量捕获机制。
不多说,先来一张图,后面再分析各自的情况:
1、捕获(capture):是指在 block 内部会专门新增一个成员变量,来存储传进来的值。或者说将外面的值捕获到 block 结构体中存储。
2、{ ... } :函数作用域,内部声明的变量都是局部变量。
3、auto:又称自动变量。其作用是离开对应的作用域该变量就会被销毁(值传递)。
(c语言中,当定义出一个局部变量的时候,会默认添加,所以不再需要手动添加)
如下面两个定义的变量是等价的:
auto int age = 0; int age = 0;
注意:auto只存在于局部变量中。
4、static:静态变量,又分静态局部变量,静态全局变量
3.1、auto 捕获(capture)
auto 的捕获结果在第【2】大点上已经分析,这里不再阐述。
3.2、static 捕获(capture)
1、代码示例
int main(int argc, const char * argv[]) { @autoreleasepool { auto int age = 10; static int height = 10; void(^block)(void) = ^{ NSLog(@"age:%d height:%d",age,height); }; age = 20; height = 20; block(); } return 0; }
// 打印结果
2022-05-07 16:58:09.938310+0800 block-01[56215:2690621] age:10 height:20
2、底层结构分析
int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; auto int age = 10; static int height = 10; void(*block)(void) = &__main_block_impl_0( __main_block_func_0, &__main_block_desc_0_DATA, age,//传递变量的值 &height//传递变量的地址 ); age = 20; height = 20; block->FuncPtr(block); } return 0; } struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; // ------- 捕获到两个成员 ------------ int 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; } }; 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 NSLog((NSString *)&__NSConstantStringImpl__var_folders_vc_pn677_yj1sz8hgf_q5bjvssc0000gn_T_main_d0ee52_mi_0, age,//取block成员变量的值 (*height)// 取出指针变量所指向内存的值 ); }
分析总结:
1、只要是局部变量而且是 block 要访问的局部变量,block 都会将这些局部变量捕获到block结构体中。
2、捕获的区别就是 auto 进行的值传递,而 static 进行的是指针传递。
3.3、局部变量(auto、static)访问方式差异的原因分析
那么为什么会有这个差异呢?
1、auto 局部变量的内存随时可能被销毁,内存会消失。所以优先保证变量的值能拿到。
2、static 地址一直存在,可以在需要的时候取值,所以这里优先保证取得的数据最新。
举例如下:
#import <Foundation/Foundation.h> // block 变量 void(^block)(void); // 函数方法 void test() { // 1.0、执行完 test 方法,该成员变量就会被销毁 auto int age = 10; // 2.0、由于 static 修饰的变量是一直在内存中的,即使test方法执行结束,这个变量还是会在内存中的 static int height = 10; block = ^{ // 此时 两个变量还能正常输出 // 1.1、auto 自动变量是因为block内部存储了值(值传递),所以这里能正常取到值。yy:既然对方随时可能跑路,那么就赶紧把属于自己的先拿到。 // 2.1、因为该成员变量的内存一直存在,所以这里可以直接取成员变量内存中的值。yy:既然对方都跑不了路,那就可以在需要的时候再去取,刚拿到的才是最新鲜的。 NSLog(@"age:%d height:%d",age,height); }; age = 20; height = 20; } int main(int argc, const char * argv[]) { @autoreleasepool { // 调用 block test(); // 执行block block(); } return 0; }
4、全局变量访问
4.1、代码示例
//全局变量 int age_ = 10; //全局静态变量 static int height_ = 10; int main(int argc, const char * argv[]) { @autoreleasepool { void(^block)(void) = ^{ NSLog(@"age:%d height:%d",age_,height_); }; age_ = 20; height_ = 20; block(); } return 0; }
编译运行打印结果:
// 两个都能正常取到改变数值后的值 2022-05-07 20:33:10.697823+0800 block-01[59547:2776769] age:20 height:20
4.2、底层结构分析
int age_ = 10; static int height_ = 10; // 程序入口 int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; void(*block)(void) = &__main_block_impl_0( __main_block_func_0, &__main_block_desc_0_DATA)); age_ = 20; height_ = 20; block->FuncPtr(block); } return 0; } // 在这里会发现,不管是全局变量还是全局静态变量,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; } }; // 从这边的函数可以看到,是直接访问全局变量的 static void __main_block_func_0(struct __main_block_impl_0 *__cself) { NSLog((NSString *)&__NSConstantStringImpl__var_folders_vc_pn677_yj1sz8hgf_q5bjvssc0000gn_T_main_ee8bdb_mi_0, age_, height_ ); } 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)};
结论:
只要是全局变量,block内部不需要去捕获这些变量,可以直接进行访问。
5、self是否会捕获都 block 里面去?
5.1、示例代码
OC:
Person.h
#import <Foundation/Foundation.h> @interface Person : NSObject @property (nonatomic,assign) int age; - (void)test; @end
Person.m
#import "Person.h" @implementation Person - (void)test { void(^block)(void) = ^{ NSLog(@"--------%p",self); }; block(); } @end
5.2、底层结构分析
/** * 默认生成两个参数 * @param self:方法调用者本身,又称对象本身。因此self本身也是一个局部变量 * @param _cmd:这个方法的方法名称 */ static void _I_Person_test(Person * self, SEL _cmd) { void(*block)(void) = &__Person__test_block_impl_0( __Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344)); block->FuncPtr(block); } struct __Person__test_block_impl_0 { struct __block_impl impl; struct __Person__test_block_desc_0* Desc; Person *self;// 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、在OC中我们可以看到,当函数方法转换成C++时,都默认带有两个参数,第一个参数是调用者本身self,第二个参数是 _cmd 这个方法的方法名称。也就是说,OC函数方法是默认有这两个参数的,只是默认隐藏起来不显示出来而已。因此我们可以知道 self 是局部变量,不是全局变量, self 指向的是方法调用者本身。
2、正因为 self 是局部变量,所以 self 会捕获到 block 里面去。