block 底层结构
block,在日常开发中经常遇到。常用的使用场景就是用于两个对象之间的交互,比如 vc 之间的传值、网络接口的回调等。
先来一个最简单的例子来看下 block 的底层结构,代码示例:
^{
NSLog(@"hello world");
}();
通过 xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp编译成 C++ 代码:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
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 的底层结构
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;
}
};
通过上面的 isa 可以看出,block 是一个 _NSConcreteStackBlock 类型的对象。
变量捕获
局部变量
- auto
int num = 1;
void (^block)(void) = ^(){
NSLog(@"num value is: %d", num);
};
num = 2;
block();
上述代码的打印结果:num value is: 1,虽然在 block 调用之前修改了 num 的值,但在打印上可以看出 num 的值仍然为 1。下面将代码编译成 C++ 来看一下原因:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 结构与上面大致相同,只多了一个 num 的变量。
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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
// 实际调用的是 block 内部捕获的 num
int num = __cself->num; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_5f_h3wcc64x7r72drwvgq5drdvh0000gp_T_main_b7ef04_mi_0, num);
}
int num = 1;
// block 声明
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, num));
在 block 的声明,可以看到局部变量 num 的值已经传递给 block 结构体 __main_block_impl_0 ,内部的 num 变量。所以,后面即使修改 num 的值,也不影响 block 里面已捕获的值。
- static
static int num = 1;
......
// 其余代码相同
输出结果:num value is: 2。可以看到这次 num 修改起作用了。还是编译成 C++ 看一下原因:
struct __main_block_impl_0 {
......
int *num;
......
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
// 取出 num 的内存地址
int *num = __cself->num; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_5f_h3wcc64x7r72drwvgq5drdvh0000gp_T_main_cede7d_mi_0, (*num));
}
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &num));
这次可以看到 num前面加了一个 * ,代表它是指针类型。从 &num 传入也可以得知它传入的是 static 修饰的 num 的内存地址。因为 __main_block_impl_0 里面的 num 和 static 修饰的 num 是指向一块内存,所以 num = 2 的修改是生效的。
全局变量
C++ 代码:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_5f_h3wcc64x7r72drwvgq5drdvh0000gp_T_main_933c32_mi_0, num);
}
可以看到,没有从 __main_block_impl_0 取值,而是直接访问的 num 。因为 num 是全局变量,所以可以直接访问。
局部变量 auto、 static;全局变量的捕获方式总结图:
block 类型
block 一共有三种类型,都继承自 NSBlock:
- NSGlobalBlock:没有访问 auto 变量的即为 global。存放在数据段。
void (^block)(void) = ^(){
NSLog(@"hello, world");
};
// __NSGlobalBlock__, NSBlock
NSLog(@"%@, %@", [block class], [[block class] superclass]);
- NSStackBlock:访问了 auto 变量的为 stack。存放在栈上。
// __NSStackBlock__, NSBlock
NSLog(@"%@,%@", [^(){ NSLog(@"num value is: %d", num); } class],
[[^(){ NSLog(@"num value is: %d", num); } class] superclass]);
- NSMallocBlock:stack 调用了 copy 函数的即为 malloc。存放在堆上。
// __NSMallocBlock__,NSBlock
NSLog(@"%@,%@", [[^(){ NSLog(@"num value is: %d", num); } copy] class],
[[[^(){ NSLog(@"num value is: %d", num); } copy] class] superclass]);
block 的 copy 操作
在 ARC 环境下,如果出现以下情况,编译器会自动将栈上的 block 拷贝到堆上,即类型从 __NSStackBlock__ 变成 __NSMallocBlock__。
- block 为方法的返回值。
// .h
typedef void (^SomeBlock) (void);
@interface Goods : NSObject
- (SomeBlock)test;
@end
// .m
- (SomeBlock)test {
int a = 1;
return ^{
NSLog(@"test -- %d", a);
};
}
// main
Goods *g = [Goods new];
NSLog(@"%@", [[g test] class]); // __NSMallocBlock__
- block 被强指针指向。
int num = 1;
void (^block)(void) = ^(){
NSLog(@"num value is: %d", num);
};
NSLog(@"%@", [block class]); //__NSMallocBlock__
- 系统方法中含有 usingBlock 的参数。
- 作为 GCD 方法的参数。
block 内部访问对象类型的 auto 变量
如果在 block 内部访问了对象类型的 auto 变量,那么在 __main_block_desc_0 结构体里面会添加 copy 、dispose 函数来进行对象的内存管理。比如下面的代码:
NSObject *obj = [NSObject new];
void (^block)(void) = ^(){
NSLog(@" %@", obj);
};
block();
编译成 C++ 的代码,结构体 __main_block_desc_0 代码如下:
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
// 对比上面的基本类型的 auto 变量,多了如下两个函数
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
如果 block 是存在栈上,那么它并不会对变量进行强引用;如果 block 在堆上的话则会根据变量的修饰符(strong/weak)来进行强或者弱引用。
当 block 在堆上时,它会调用内部的 copy 函数,copy 内部调用 _Block_object_assign 函数来进行对象变量的内存管理。
当 block 在堆上移除时,它会调用内部的 dispose 函数,dispose 内部调用 _Block_object_dispose 函数来释放对象变量。
__block
使用场景:解决 block 内部无法修改 auto 变量值的问题,比如下面的代码:
NSObject *obj = [NSObject new];
void (^block)(void) = ^(){
obj = nil;
NSLog(@" %@", obj);
};
block();
编译器会报错:Variable is not assignable (missing __block type specifier)。obj 添加一个 __block 就可解决该问题。
如果没有添加 __block,block 内部会捕获 obj 的值,并在堆上开辟一块内存空间来存储。外部的 obj 存放在栈上,内部的 obj 存放在堆上,虽然它俩名字一样,但并不是一块地址。所以在内部不能修改 obj 的值。
未添加 __block 打印 obj 的内存地址:
NSObject *obj = [NSObject new];
NSLog(@"0000 %p", &obj); // 0x7ffeefbff3f8 栈
void (^block)(void) = ^(){
NSLog(@"1111 %p", &obj); // 0x10304d6f0 堆
};
block();
NSLog(@"2222 %p", &obj); // 0x7ffeefbff3f8 栈
添加 __block 之后,__block 会捕获 obj 的内存地址,并在堆上开辟一块内存空间来存储 obj,然后将 block 外部的变量内存地址也改成堆上的。这样内部的 obj 和外部的 obj 指向的是同一块内存,自然就可以在内部修改 obj 的值了。
添加 __block 打印 obj 的内存地址:
__block NSObject *obj = [NSObject new];
NSLog(@"0000 %p", &obj); // 0x7ffeefbff3f8 栈
void (^block)(void) = ^(){
NSLog(@"1111 %p", &obj); // 0x100507ce8 堆
};
block();
NSLog(@"2222 %p", &obj); // 0x100507ce8 栈
上述图示:
需要注意的是,__block只能修饰 auto 变量不可修饰 static 和全局变量。
循环引用
block 的使用过程中,代码不规范可能会造成循环引用,从而导致内存泄漏。比如下面的代码:
@interface Goods : NSObject
@property(nonatomic, copy)SomeBlock block;
@property(nonatomic, copy)NSString *name;
@end
Goods *g = [Goods new];
g.block = ^{
NSLog(@"%@", g.name);
};
因为 g 强引用了 block,而 block 内部又强引用了 g,形成循环引用导致 g 无法被释放。可以通过 __weak、__unsafe_unretained、__block 解决。再次只用 __weak 举例:
Goods *g = [Goods new];
__weak typeof(g) weakG = g;
g.block = ^{
NSLog(@"%@", weakG.name);
};
总结
- 局部 auto 变量为值捕获;局部 static 变量为指针捕获;全局变量直接访问。
- block 三种类型 Global、Stack、Malloc,都继承自 NSBlock。
- block 为返回值、强指针指向和某些系统方法的参数时,编译器会自动从栈上拷贝到堆上。
- __block 修饰的 auto 变量, 可以在 block 内部修改 auto 变量值。
- __weak/__unsafe_unretained/__block 用来解决循环引用问题。