✅作者简介:大家好我是瓜子三百克,一个非科班出身的技术程序员,还是喜欢在学习和开发中记录笔记的博主小白!
📃个人主页:瓜子三百克的主页
🔥系列专栏:OC语法
🤟格言:作为一个程序员都应该认识到,好的代码是初级工程师都可以理解的代码, 伟大的代码是可以被学习一年计算机专业的新生所理解。
💖如果觉得博主的文章还不错的话,请点赞👍+收藏⭐️+留言📝支持一下博主哦🤞
让我们一起卷起来吧!!!
上面一节讲解了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、代码示例
- 1.2、代码转成C++
- 1.3、底层结构分析
- 2、block内部引用局部变量
- 2.1、代码示例
- 2.2、底层结构分析
- 3、block的变量捕获(capture)
- 3.1、auto 捕获(capture)
- 3.2、static 捕获(capture)
- 1、代码示例
- 2、底层结构分析
- 3.3、局部变量(auto、static)访问方式差异的原因分析
- 4、全局变量访问
- 4.1、代码示例
- 4.2、底层结构分析
- 5、self是否会捕获都 block 里面去?
- 5.1、示例代码
- 5.2、底层结构分析
#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 内部成员变量的值。
那么问题就来了,怎么让 block 内部能够正常访问外部的变量。
这里就涉及到了block变量捕获机制。
不多说,先来一张图,后面再分析各自的情况:
1、捕获(capture):是指在 block 内部会专门新增一个成员变量,来存储传进来的值。或者说将外面的值捕获到 block 结构体中存储。
2、{ ... }
:函数作用域,内部声明的变量都是局部变量。
3、auto
:又称自动变量。其作用是离开对应的作用域该变量就会被销毁(值传递)。
(c语言中,当定义出一个局部变量的时候,会默认添加,所以不再需要手动添加)
如下面两个定义的变量是等价的:
auto int age = 0;
int age = 0;
注意:auto只存在于局部变量中。
4、static
:静态变量,又分静态局部变量,静态全局变量
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 进行的是指针传递。
那么为什么会有这个差异呢?
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内部不需要去捕获这些变量,可以直接进行访问。
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 里面去。
**🏆结束语🏆 **
最后如果觉得我写的文章对您有帮助的话,欢迎点赞✌,收藏✌,加关注✌哦,谢谢谢谢!!
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)