变量分类
在了解变量捕获之前,我们首先了解一下C语言中变量的分类。C语言中变量分为三类
- 全局变量: 作用域在全局,哪个地方都能调用
- 局部变量:作用域在大括号中,只能在大括号内调用
- 局部自动变量
auto
关键字修饰 - 局部静态变量
static
关键字修饰
- 局部自动变量
block变量捕获分类
// 全局变量
int a = 10;
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 自动变量
auto int b = 20;
// 自动变量 前边使用auto关键字修饰 系统默认变量是auto 所以我们一般省略auto关键字
int c = 20;
// 静态变量
static int d = 30;
}
return 0;
}
自动变量前边使用auto关键字修饰 系统默认变量是auto 所以我们一般省略auto关键字,上方的b和c都是自动变量。表示静态变量的static关键字是不能省略的
block变量捕获机制
变量类型 | 是否能捕获 | 访问方式 |
---|---|---|
全局变量 | ❌ | 直接访问 |
局部自动变量(auto) | ✅ | 值传递 |
局部静态变量(static) | ✅ | 地址传递 |
全局变量捕获
block其实不用捕获全局变量,标题只是为了分类讨论,大家不用纠结。
// 全局变量
int num = 10;
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^block)(void) = ^{
NSLog(@"num is %d", num);
};
block();
}
return 0;
}
看下C++源码 关于如果获取C++源码 请自行百度或者查看我的上一篇文章Block本质,文章中简单说明了一下clang指令用法
#pragma clang assume_nonnull end
// 这里就是定义的全局变量
int num = 10;
// 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内部调用方法
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
// 打印的num值
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_r_tmjs697cnd9ry_p3npfzzc0000gn_T_main_35fc61_mi_0, num);
}
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)};
// main函数
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
我们可以看到在 __main_block_func_0
调用NSLog
访问num
的时候,是直接访问的全局的那个num变量。block没有捕获这个全局变量num
局部自动auto
变量捕获
以下变量捕获我们只展示不同的内容
int main(int argc, const char * argv[]) {
@autoreleasepool {
int num = 10;
void(^block)(void) = ^{
NSLog(@"num is %d", num);
};
num = 20;
block();
}
return 0;
}
// 最终打印结果
// num is 10
编译后
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 这里就是捕获的变量类型
int num;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _num, int flags=0) : num(_num) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int num = 10;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, num));
num = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
如箭头所指,block实现方法是把外部auto变量num的值传递进来了,然后在__main_block_impl_0
内部定义中多了一个 int num
变量。并且初始化的时候__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _num, int flags=0) : num(_num)
是把传递过来的_num
赋值给了num
调用过程
// block 内部调用
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
/* ⚠️⚠️⚠️⚠️ 喂喂喂 注意看这里 ⚠️⚠️⚠️⚠️
*当block内部调用方法的时候是拿到block的成员变量num 赋值下方临时定义的num 然后调用NSlog方法 打印临时的num值
*/
int num = __cself->num; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_r_tmjs697cnd9ry_p3npfzzc0000gn_T_main_e46c57_mi_0, num);
}
当调用block的时候 我们可以看到int num = __cself->num;
block把自己生成的成员变量num赋值给了新变量num,然后调用NSLog的时候直接使用了这个新变量num。main
函数中 num = 20 是在block捕获变量num=10之后,所以不会修改block内部捕获的num=10这个值。所以最终结果num=20。
局部自动static
变量捕获
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 静态变量
static int num = 10;
void(^block)(void) = ^{
NSLog(@"num is %d", num);
};
num = 20;
block();
}
return 0;
}
// 打印结果
// num is 20
编译后
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 这里就是捕获的变量类型
int *num;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_num, int flags=0) : num(_num) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
static int num = 10;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &num));
num = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
注意看箭头,这里传递的是num变量的地址,我们也可以仔细观察,在
__main_block_impl_0
内部定义中多了一个 int *num
变量,这里的num也是地址指针。这就说明static变量捕获的是变量的地址而不是变量的值
调用
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *num = __cself->num; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_r_tmjs697cnd9ry_p3npfzzc0000gn_T_main_2d4953_mi_0, (*num));
}
通定义一样,调用的时候也是调用的num变量的地址,这也就是说只要num指针指向的那片内存空间的值发生变化的时候,block内部也会发生改变,所以这次最终打印结果为 num is 20.
__block
修饰的变量
block修饰的变量实际上是非常重要的,我可能会单独写一篇文章介绍一下,在这里只是简单做一下分析
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int num = 10;
void(^block)(void) = ^{
NSLog(@"num is %d", num);
};
num = 20;
block();
}
return 0;
}
编译后
// 新生成的对象
struct __Block_byref_num_0 {
void *__isa;
__Block_byref_num_0 *__forwarding;
int __flags;
int __size;
int num;
};
// block 实现
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 这是捕获的类型
__Block_byref_num_0 *num; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_num_0 *_num, int flags=0) : num(_num->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_num_0 num = {(void*)0,(__Block_byref_num_0 *)&num, 0, sizeof(__Block_byref_num_0), 10};
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_num_0 *)&num, 570425344));
(num.__forwarding->num) = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
我们可以看到对于__block
修饰的变量,block捕获后会重新生成一个__Block_byref_num_0
新类型,里边包含一个isa
指针,也就是生成了一个对象类型的变量。
变量捕获与修改外部变量值
局部自动变量修改值
int num = 10;
void(^block)(void) = ^{
num = 20; /*这句代码会引起报错*/
NSLog(@"num is %d", num);
};
局部自动变量捕获的是变量的值,也就是上方例子中的10这个具体数值,所以block内部不能修改外部变量的值
局部静态变量修改值
static int num = 10;
void(^block)(void) = ^{
num = 20;
NSLog(@"num is %d", num);
};
// num is 20
局部static变量捕获的是变量的内存地址,也就是外部num与内部num指向的是同一个地址,内部num修改了内存地址中的值,那么外部这个num也就修改了。
__block修饰的变量
__block int num = 10;
void(^block)(void) = ^{
num = 20;
NSLog(@"num is %d", num);
};
// num is 20
__block
把变量包装成了对象类型,这里其实也是可以修改值的,上方只是简单介绍了一下,后期可能会单独研究__block
总结
- 全局变量不用捕获,可以直接访问
- 局部自动
auto
变量,捕获的是变量的具体值,block内部无法修改外部的值。 - 局部静
static
态变量,捕获的变量的地址,内部可以修改外部变量的值。 -
__block
修饰的变量,block内部把变量包装成了对象,也是可以修改值的。
有什么不正确的地方,欢迎大家指正,大家加油!!!