探究 Block 的奥秘
闲来无事,总结了一下 block 的几点知识,以作巩固,欢迎指正。
一、block 的本质
block 本质上是一个 OC 对象,它内部有一个 isa 指针。
block 是封装了函数调用以及函数环境的 OC 对象。
要研究 block 在底层编译器具体的源码实现方式,可以使用 llvm 编译器中的 clang 命令clang-rewrite-objc main.m
查看,它会将 OC 的源码 main.m 文件改写成 C++ 的 main.cpp 文件(但只可作为参考,因为 llvm 编译器生成的中间文件和 C++ 文件还是有所差异的)。更加完整的命令是:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
,感兴趣的可以试一试。
从源码中可以发现,block 在转换成 C++ 的时候显示为:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr; //block中调用的函数的地址
};
// block的描述信息
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size; //block所占的内存大小
}
// block的源码结构
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
};
显而易见,block 结构体的第一个元素是结构体 __block_impl,而 __block_impl 结构体的第一个元素是 isa,由此可知 block 实际上是一个 OC 对象。
其次,我们都知道 OC 对象都有其类型,可调用-class
方法查看对象所属的类。
void (^block)(void) = ^{
NSLog(@"hello");
};
NSLog(@"%@", [block class]);
NSLog(@"%@", [[block class] superclass]);
NSLog(@"%@", [[[block class] superclass] superclass]);
NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
控制台打印结果如下:
image.png
明显看出,__NSGlobalBlock __ 继承自__NSGlobalBlock,__NSGlobalBlock 继承自 NSBlock,NSBlock 继承自NSObject,即 block 所属的类最终是继承自 NSObject 的,所以说 block 是 OC 对象。
二、block分类
OC 中有3种类型的 block,可以通过调用 class 方法或者 isa 指针查看具体类型,最终都是继承自 NSBlock 类。
1、_NSConcreteGlobalBlock 全局的静态 block,在常量区(数据区域),不会访问任何外部变量。
2、_NSConcreteStackBlock 保存在栈中的 block,当函数返回时会被销毁。
3、_NSConcreteMallocBlock 保存在堆中的 block,动态分配内存,当引用计数为 0 时会被销毁。
// _NSConcreteGlobalBlock:没有访问auto变量
void (^block)(void) = ^{
NSLog(@"hello");
};
NSLog(@"%@", [block class]); //输出__NSGlobalBlock__
// _NSConcreteStackBlock :访问了auto变量
int a = 10;
NSLog(@"%@", [^{
NSLog(@"hello %d", a);
} class]); //输出__NSStackBlock__
// _NSConcreteMallocBlock:__NSStackBlock__调用copy方法
int a = 10;
void (^block)(void) = [^{
NSLog(@"hello %d", a);
} copy];
NSLog(@"%@", [block class]);//输出__NSMallocBlock__
每一种类型的 block,调用 copy 后的结果如下所示:
image.png
注意!在ARC环境下,编译器会根据情况自动将栈上的 block 复制到堆上,比如以下情况:
1、block 作为函数返回值时
2、将 block 赋值给 __strong 指针时
3、block 作为 Cocoa API 中方法名含有 usingBlock 的方法参数时
4、block 作为 GCD API 的方法参数时
当 block 内部访问了对象类型的 auto 变量的时候,会产生两种可能:
1、如果 block 是在栈上,将不会对 auto 变量产生强引用;
2、如果 block 被拷贝到堆上,会调用 block 内部的copy
函数,copy
函数内部会调用_Block_object_assign
函数,_Block_object_assign
函数会根据 auto 变量的修饰符(__strong、__weak、__unsafe_unretained)操作,类似于 retain (形成强引用、弱引用)。
如果 block 从堆上移除,会调用 block 内部的dispose
函数,dispose
函数内部会调用_Block_object_dispose
函数,_Block_object_dispose
函数会自动释放引用的 auto 变量,类似于 release。
三、block 的变量捕获
问题1:下面的代码输出结果为何?
int a = 10;
void (^block)(void) = ^{
NSLog(@"a = %d", a);
};
a = 20;
block();
通过终端命令,可以查看到编译后的最终代码如下:
// 当前block的结构
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
int a = 10;
//此时传入的参数a=10,通过__main_block_impl_0函数传入的最后一个参数为a,被block结构体内部的int a;元素所捕获,故此block结构体内部的a元素保存的是传入的参数a的值
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
//再修改外部参数a=20也无用,因为这并不影响block结构体内部a元素的值
a = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
在源码的注释中已经解释了实际的执行逻辑,故此可以看出输出的结果为:
image.png
问题2:若将上述问题中的int a = 10;
修改成static int a = 10;
,结果是否会有所不同?
同上,粘代码便一目了然(截取部分 OC 代码与 C++ 代码)。
/***** 编译前的OC代码 *****/
static int a = 10;
void (^block)(void) = ^{
NSLog(@"a = %d", a);
};
a = 20;
block();
/***** 编译后的C++代码 *****/
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static int a = 10;
//此时传入的参数a=10,通过__main_block_impl_0函数传入的最后一个参数为&a,被block结构体内部的int *a;元素所捕获,故此block结构体内部的指针a元素保存的是传入的参数a的地址值
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &a));
//再修改外部参数a=20,在调用block函数时,因其结构体内部a元素保存的是外部参数a的地址值,此时该地址值所指向的外部参数a=20
a = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
在源码的注释中已经解释了实际的执行逻辑,故此可以看出输出的结果为:
image.png
问题3:若将上述问题中的int a = 10;
声明为全局变量,结果又是否会有所不同?
同上,粘代码便一目了然(截取部分 C++ 代码)。
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;
}
};
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
a = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
很明显,block 结构体内部并没有捕获全局变量a的值或者地址,而是直接访问全局变量的,故此可以看出输出的结果为:
image.png
总结:
为了保证 block 内部能够正常访问外部的变量,block 有个变量捕获机制:
全局变量:不捕获到 block 内部,直接访问
局部变量:捕获到 block 内部,其中auto变量通过值传递访问,static变量通过指针传递访问
备注:auto自动变量,离开作用域就会被销毁。
- (void)blockTest {
// block外面的a和block内部的a实际上不是同一个对象
int a = 10; //a在栈上
NSLog(@"outside a === %p", &a);
void (^blocka)(void) = ^(void) {
NSLog(@"a ==== %p", &a); //a被copy了一份,实际上C方法的代码块中传递的是copy得到的a的值
};
blocka();
__block int b = 10; //b在栈上
NSLog(@"outside b === %p", &b);
void (^blockb)(void) = ^(void) {
NSLog(@"b ==== %p", &b); //b被copy到堆上,实际上C方法的代码块中传递的是copy得到的b的指针地址
};
blockb();
}
四、__block 的作用
int age = 10;
void (^block)(void) = ^{
NSLog(@"age is %d", age);
};
block();
毫无疑问,控制台会输出 “age is 10”。如果想在 block 代码块中修改局部变量 age 的值为20,该怎么做?
直接修改肯定会报错,当然可以直接将 age 定义为 static 变量或者全局变量,问题就会迎刃而解。但是这样的话,age 就会一直存在在内存的全局区中,这并不是最理想的状态,希望 age 还是一个临时变量,在不用的时候会自动销毁。这时候可以使用 __block 修饰 age 即可。
__block int age = 10;
void (^block)(void) = ^{
age = 20;
NSLog(@"age is %d", age);
};
block();
此时控制台会输出 “age is 20”。用 __block 的好处是不会修改变量的性质,age 还是一个 auto 类型的自动变量。
__block 可以用来用于解决 block 内部无法修改 auto 变量值的问题。
__block 不能修饰全局变量、静态变量(static)。
编译器会将 __block 变量包装成一个对象。
上面的代码中__block int age = 10;
会编译成下面的代码结构:
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding; //指向结构体自身的指针
int __flags;
int __size;
int age; //这个age才是对应外部 __block 修饰的局部变量,共享同一块内存地址
};
//block 内部有个 *age 指针,指向 __Block_byref_age_0 结构体,__Block_byref_age_0 结构体内部有 int age ;通过 __Block_byref_age_0 结构体的 __forwarding 指针找到其内部 age 的内存地址,并修改其值,完成修改。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // by ref
};
__block 的内存管理
当 block 在栈上时,并不会对 __block 变量产生强引用。
当 block 被拷贝到堆上时,会调用 block 内部的copy
函数,copy
函数内部会调用_Block_object_assign
函数,_Block_object_assign
函数会对 __block 变量形成强引用(retain)。
五、block 的使用
1、作为对象的属性
用 block 作为属性,保存一段代码块,可以在任何想用它的地方随时使用。MVVM 设计模式中,在绑定 view 和 VM 的时候就常用到。
#import <Foundation/Foundation.h>
@interface Person : NSObject
// ARC下block可以用strong修饰,MRC下用copy修饰
@property (nonatomic, strong) void(^myName)(NSString * name);
@end
在ViewController
中使用即可:
- (void)viewDidLoad {
[super viewDidLoad];
Person * p = [[Person alloc] init];
p.myName = ^(NSString *name) {
NSLog(@"你好,我的名字叫%@", name);
};
p.myName(@"Rac");
}
打印结果如下:
image.png
2、作为方法的参数
#import <Foundation/Foundation.h>
@interface Person : NSObject
- (void)eat:(void (^)(void))block;
@end
#import "Person.h"
@implementation Person
- (void)eat:(void (^)(void))block {
//1、保存这个block
//2、做特定的事情
//3、得到一个结果,将结果给block
block();
}
@end
在ViewController
中调用即可:
- (void)viewDidLoad {
[super viewDidLoad];
Person * p = [[Person alloc] init];
[p eat:^{
NSLog(@"我要吃东西");
}];
}
打印结果如下:
image.png
3、作为方法的返回值
block作为方法的返回值,可以通过点语法进行调用,进而实现链式编程功能,我们常用的自动布局第三方Mansory
的实现就是这个原理。
#import <Foundation/Foundation.h>
@interface Person : NSObject
//打点调用,必须是getter方法,满足两点:1、有返回值;2、没有参数
- (void(^)(int))run;
// 持续打点调用(即链式编程),block返回当前类的对象即可
- (Person *(^)(int))runAgain;
@end
#import "Person.h"
@implementation Person
- (void (^)(int))run {
return ^(int m){
NSLog(@"我跑了%d米", m);
};
}
- (Person *(^)(int))runAgain {
return ^(int m){
NSLog(@"我这次又跑了%d米", m);
return self;
};
}
@end
在ViewController
中调用即可:
- (void)viewDidLoad {
[super viewDidLoad];
Person * p = [[Person alloc] init];
p.run(10);
// p.run(10)相当于:
// void (^blocka)(int m) = p.run;
// blocka(10);
p.runAgain(10).runAgain(20).runAgain(30);
}
打印结果如下:
image.png
六、block 循环引用问题
循环引用,肯定大家都知道的,通俗点说,就是A持有B,B持有A,互相强引用,导致双方(或多方)都无法正常释放,从而引起内存泄漏。
我这里也提供三种解决block循环引用的方法,当然还有其他的解决办法,原则就一个:打破这个循环链!
1、__weak
__weak typeof (self) weakself = self;
self.vblock = ^(){
weakself.view.backgroundColor = [UIColor yellowColor];
[weakself.navigationController popViewControllerAnimated:YES];
};
self.vblock();
2、__block
注意:blockSelf在代码块中用要完置为nil
__block ViewController * vself = self;
self.vblock = ^{
vself.view.backgroundColor = [UIColor yellowColor];
[vself.navigationController popViewControllerAnimated:YES];
vself = nil;
};
self.vblock();
3、self作为block的参数传递进去
self.sblock = ^(ViewController * vc) {
vc.view.backgroundColor = [UIColor yellowColor];
[vc.navigationController popViewControllerAnimated:YES];
};
self.sblock(self);
检测是否解决循环引用问题,只需查看dealloc
方法是否被调用即可,因为当前对象被销毁时肯定会走dealloc
方法,大家可自行测试。