笔者前几天做了一道笔试题:默认情况下 block 是不能修改外面的 auto 变量的,解决办法? 思索了好久才想起可以用 __block 或者 static 修饰符来修饰 auto 变量。看来有必要好好探索一下 block 的相关知识。
目录
1. block 的使用
1.1 什么是 block ?
block 的声明
block 的定义
block 的调用
2. block 的底层数据结构
3. block 的变量捕获机制
3.1 auto 自动变量
3.2 static 类型的局部变量
3.3 全局变量
为什么局部变量需要捕获,而全局变量不用呢?
3.4 __block 修饰的变量
3.4.1 __block 的使用:
3.4.2 __block 修饰符
1. block 的使用
1.1 什么是 block ?
块,封装了函数调用以及调用环境的 OC 对象。
-
block 的声明
// 1.
@property (nonatomic, copy) void(^myBlock1)(void);
// 2.BlockType:类型别名
typedef void(^BlockType)(void);
@property (nonatomic, copy) BlockType myBlock2;
// 3.
// 返回值类型(^block变量名)(参数1类型,参数2类型,...)
void(^block)(void);
-
block 的定义
// ^返回值类型(参数1,参数2,...){};
// 1.无返回值,无参数
void(^block1)(void) = ^{
};
// 2.无返回值,有参数
void(^block2)(int) = ^(int a){
};
// 3.有返回值,无参数(不管有没有返回值,定义的返回值类型都可以省略)
int(^block3)(void) = ^int{
return 3;
};
// 以上Block的定义也可以这样写:
int(^block4)(void) = ^{
return 3;
};
// 4.有返回值,有参数
int(^block5)(int) = ^int(int a){
return 3 * a;
};
-
block 的调用
// 1.无返回值,无参数
block1();
// 2.有返回值,有参数
int a = block5(2);
2. block 的底层数据结构
通过 Clang 将以下的 OC 代码转换为 C++代码
// Clang
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
//main.m
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^block)(void) = ^{
NSLog(@"我是 block");
};
block();
}
return 0;
}
转换为 C++ 代码 :
struct __main_block_impl_0 {
struct __block_impl impl;// block 结构体
struct __main_block_desc_0* Desc;// block 的描述对象
/*
block 的构造函数
** 返回值:__main_block_impl_0 结构体
** 参数一:__main_block_func_0 结构体
** 参数二:__main_block_desc_0 结构体的地址
** 参数三:flags 标识位
*/
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;// &_NSConcreteStackBlock 表示存储在栈上
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//block 结构体
struct __block_impl {
void *isa;//block 的类型
int Flags;
int Reserved;
void *FuncPtr;// block的执行函数指针,指向__main_block_func_0
};
//封装了 block 中的代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_yx_7jg_wdg128v45l4cn_1g265h0000gn_T_main_03dcda_mi_0);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;// block 所占的内存空间
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
/*
** void(^block)(void) = ^{
NSLog(@"调用了block");
};
** 定义block的本质:
** 调用__main_block_impl_0()构造函数
** 并且给它传了两个参数 __main_block_func_0 和 &__main_block_desc_0_DATA
** __main_block_func_0 封装了block里的代码
** 拿到函数的返回值,再取返回值的地址 &__main_block_impl_0,
** 把这个地址赋值给 block
*/
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
/*
** block();
** 调用block的本质:
** 通过 __main_block_impl_0 中的 __block_impl 中的 FuncPtr 拿到函数地址,直接调用
*/
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
3. block 的变量捕获机制
- 对于全局变量,不会捕获到 block 内部,访问方式为直接访问;
- 对于 auto 类型的局部变量,会捕获到 block 内部,block 内部会自动生成一个成员变量,访问方式为值传递;
- 对于 static 修饰的局部变量,会捕获到 block 内部,block 内部会自动生成一个成员变量,访问方式为指针传递。
- 对于对象类型的局部变量,block 会连同其所有权修饰符一起捕获。
3.1 auto 自动变量
将以下 OC 代码转换为 C++ 代码,并贴出部分变动代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
void(^block)(void) = ^{
NSLog(@"%d",age);
};
block();
}
return 0;
}
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
__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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_yx_7jg_wdg128v45l4cn_1g265h0000gn_T_main_40c716_mi_0,age);
}
可以看出:
- 在 __main_block_impl_0 结构体中会自动生成一个相同的 age 变量
- 构造函数 __main_block_impl_0( ) 中多出了一个 age 参数,用来捕获外部的变量
由于是传递方式为值传递,所以我们在 block 外部修饰 age 变量时,不会影响到 block 中的 age 变量
3.2 static 类型的局部变量
将以下 OC 代码转换为 C++ 代码,并贴出部分变动代码
static int age = 10;
void(^block)(void) = ^{
NSLog(@"%d",age);
};
age = 20;
block();
// 20
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *age;
__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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_yx_7jg_wdg128v45l4cn_1g265h0000gn_T_main_cb7943_mi_0,(*age));
}
可以看出:
- __main_block_impl_0 结构体中生成了一个相同类型的 age 指针
- __main_block_impl_0 构造函数多了个参数,用来捕获外部的 age 变量的地址
由于传递方式是指针传递,所以修改 局部变量 age 时,age 的值会随之变化
3.3 全局变量
将以下 OC 代码转换为 C++ 代码,并贴出部分变动代码
int height = 10;
static int age = 20;
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^block)(void) = ^{
NSLog(@"%d,%d",height,age);
};
block();
}
return 0;
}
int height = 10;
static int age = 20;
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_yx_7jg_wdg128v45l4cn_1g265h0000gn_T_main_7a340f_mi_0,height,age);
}
可以看出:
- __main_block_impl_0 结构体中,并没有自动生成 age 或 height 全局变量,也就是说没有将变量捕获到 block 内部
那么就有一个问题:
为什么局部变量需要捕获,而全局变量不用呢?
- 作用域的原因,全局变量哪里都可以直接访问,所以不用捕获;
- 局部变量,外部不能直接访问,所以需要捕获;
- auto 类型的局部变量可能会销毁,其内存会消失,block 将来执行代码的时候不可能再去访问那块内存,所以捕获其值;
- static 变量会一直保存在内存中, 所以捕获其地址即可。
3.4 __block 修饰的变量
3.4.1 __block 的使用:
终于来到那个问题:
默认情况下 block 是不能修改外面的 auto 变量的,解决办法?
- 变量用 static 修饰(原因:捕获 static 类型的局部变量是指针传递,可以访问到该变量的内存地址)
- 全局变量
- __block(我们只希望临时用一下这个变量临时改一下而已,而改为 static 变量和全局变量会一直在内存中)
3.4.2 __block 修饰符
- __block 可以用于解决 block 内部无法修改 auto 变量值的问题;
- __block 不能修饰全局变量、静态变量;
- 编译器会将 __block 变量包装成一个对象(
struct __Block_byref_age_0
(byref:按地址传递)); - 加 __block 修饰不会修改变量的性质,它还是 auto 变量;
- 一般情况下,对被捕获变量进行赋值(赋值!=使用)操作需要添加 __block 修饰符。比如给数组添加或者删除对象,就不用加 __block 修饰;
- 在 MRC 下使用 __block 修饰对象类型,在 block 内部不会对该对象进行 retain 操作,所以在 MRC 环境下可以通过 __block 解决循环引用的问题。
将以下 OC 代码转换为 C++ 代码,并贴出部分变动代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 10;
void(^block)(void) = ^{
age = 20;
NSLog(@"block-%d",age);
};
block();
NSLog(@"%d",age);
}
return 0;
}
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // bound by ref
(age->__forwarding->age) = 20;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_yx_7jg_wdg128v45l4cn_1g265h0000gn_T_main_75529b_mi_0,(age->__forwarding->age));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}
可以看出:
- 编译器会将 __block 修饰的变量包装成一个
__Block_byref_age_0
对象; - 以上
age = 20;
的赋值过程为:通过 block 结构体里的(__Block_byref_age_0
)类型的 age 指针,找到__Block_byref_age_0
结构体的内存(即被 __block 包装成对象的内存),把__Block_byref_age_0
结构体里的 age 变量的值改为20。