面试题引发的思考:
Q: block的原理是怎样的?本质是什么?
- block本质上也是一个OC对象,它内部也有一个
isa
指针。 - block是封装了 函数调用 以及 函数调用环境 的OC对象。
1. block原理
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
void (^block)(int, int) = ^(int a, int b){
NSLog(@"this is block, a = %d, b = %d", a, b);
NSLog(@"this is block, age = %d", age);
};
age = 20;
block(1, 2);
}
return 0;
}
使用命令行xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
将代码转化成C++语言:
将C++中强制转换代码去掉,以便阅读:
(1) 定义block
代码显示:
- 定义block时调用了
__main_block_impl_0
函数,并将__main_block_impl_0
函数地址赋值给了block。
即:block底层就是__main_block_impl_0
结构体。
通过__main_block_impl_0
找到__main_block_impl_0
函数:
代码显示:
__main_block_imp_0
结构体的第一个成员是__block_impl
结构体变量,__block_impl
结构体的第一个成员是isa
;
即:__main_block_imp_0
结构体第一个成员是isa
,也就是说block的底层就是一个OC对象。
__main_block_imp_0
结构体内部有一个同名构造函数__main_block_imp_0
,会对相关变量赋值并返回一个__main_block_imp_0
结构体,然后将结构体的地址赋值给block。
定义block时__main_block_impl_0
函数传入的3个参数:
1> _main_block_func_0
参数:
很明显,其中的NSLog(...);
即我们写的NSLog(@"a = %d, b = %d, age = %d", a, b, age);
语句;
即:_main_block_func_0
函数把Block中要执行的代码封装到其内部。
2> &_main_block_desc_0_DATA
参数:
__main_block_desc_0
中包含两个参数:
a> reserved
:赋值为0
;
b> Block_size
:存储__main_block_impl_0
占用的空间大小。
3> age
参数:
age
是我们定义的局部变量。block中使用age
,所以block会在声明的时候将age
作为参数传入,即block会捕获age
。
因为block在定义时将age
值传入存储在__main_block_impl_0
结构体中,并在调动block时将age
取出来使用,所以在block定义结束后对局部变量进行修改是无法被block捕获的。所以文章开始的代码中输出的age
值为10
,而非20
。
下面再看__main_block_impl_0
函数:
由以上分析可知:
__block_impl
结构体中isa
指针存储着_NSConcreteStackBlock
地址;_main_block_func_0
函数把Block中要执行的代码封装到其内部,FuncPtr
则存储着__main_block_func_0
函数的地址;Desc
指向__main_block_desc_0
结构体对象,其中存储__main_block_impl_0
结构体所占用的内存。
(2) 调用block
// 调用block内部的代码
// 简化版:block->FuncPtr(block, 1, 2);
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 1, 2);
通过__main_block_impl_0
函数结构可知:
FuncPtr
是_main_block_impl_0
第一个成员变量impl
的成员变量,但是block调用时却直接通过block找到FuncPtr
进行调用,这是为什么呢?
原因在于:(__block_impl *)block
将block强制转化为__block_impl
类型,而impl
是__main_block_impl_0
结构体的第一个成员,所以impl
和__main_block_impl_0
的首地址是一样的,因此指向_main_block_impl_0
的首地址的指针也就可以被强制转换为指向impl
的首地址的指针,并找到FuncPtr
。
而FuncPtr
则存储着__main_block_func_0
函数的地址,因此通过block->FuncPtr()
获取__main_block_func_0
的地址,对其进行调用,进而执行block中的代码。并且_main_block_func_0
函数第一个参数就是__main_block_impl_0
类型的指针,也就是说将block传入__main_block_func_0
函数中,进而取出block捕获的值。
(3) 总结
通过以上分析,我们对block底层结构有了基本的了解,由此可以分析出其中的结构关系:
2. block的变量捕获
为了保证block内部能够正常访问外部的变量,block有个变量捕获机制(capture)。
// 全局变量c, d
int c = 30;
static int d = 40;
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 局部变量a, b
// auto变量是声明在函数内部的变量,离开作用域就销毁
// int a = 10; 则a为auto变量,会自动在前面添加auto关键字
auto int a = 10;
static int b = 20;
void (^block)(void) = ^{
NSLog(@"a = %d, b = %d, c = %d, d = %d", a, b, c, d);
};
a = 1;
b = 2;
c = 3;
d = 4;
block();
}
return 0;
}
// 打印结果
Demo[1234:567890] a = 10, b = 2, c = 3, d = 4
OC代码转化为C++查看变量调用方式:
(1) 局部变量
1> auto
变量:
auto
变量是声明在函数内部的变量,离开作用域就销毁,局部变量前面自动添加auto
关键字。auto
变量会捕获到block内部,即block内部会专门新增加一个参数来存储变量的值。auto
变量只存在于局部变量中,访问方式为值传递。
2> static
变量:
static
变量在变量的作用域结束时并不会被系统自动回收static
变量会捕获到block内部,即block内部会专门新增加一个参数来存储变量的值的地址。static
变量只存在于局部变量中,访问方式为地址传递。
(2) 全局变量
全局变量在哪里都可以访问,所以block不用捕获全局变量,直接进行访问。
(3) 总结
- block处理方式不同是由变量的声明周期决定的;
- 局部变量都会被block捕获,
auto
变量值传递,static
变量指针传递;- 全局变量不会被block捕获,直接访问。
Q: 那么以下情况block是否会捕获变量呢?
#import "Person.h"
@implementation Person
- (void)test {
void (^block)(void) = ^{
NSLog(@"-------- %p", self);
};
block();
}
@end
OC代码转化为C++查看变量调用方式:
由图可知:self
会被block捕获。
因为OC方法会默认传递两个参数self
和_cmd
,两者都是局部变量,与我们的结论:局部变量会被block捕获符合。
3. block类型
前文可知:block的本质就是一个OC对象,所以block有类型。
下面我们探寻一下block的类型,首先关闭ARC:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// __NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject
void (^block)(void) = ^{
NSLog(@"Hello");
};
NSLog(@"%@", [block class]);
NSLog(@"%@", [[block class] superclass]);
NSLog(@"%@", [[[block class] superclass] superclass]);
NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
}
return 0;
}
// 打印结果
Demo[1234:567890] __NSGlobalBlock__
Demo[1234:567890] __NSGlobalBlock
Demo[1234:567890] NSBlock
Demo[1234:567890] NSObject
由打印结果可知:
block最终继承自NSBlock
,而NSBlock
继承自NSObject
,所以block的isa
指针是来自于NSObject
。也印证了block的本质就是OC对象。
(1) block的类型及存放区域:
- block有三种类型:
__NSGlobalBlock__
、__NSMallocBlock__
、__NSStackBlock__
- 数据段中的
__NSGlobalBlock__
直到程序结束才会被回收;- 堆中的
__NSMallocBlock__
需要手动内存管理;- 栈中的
__NSStackBlock__
作用域执行完毕被回收。
(2) block类型总结:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 1. __NSGlobalBlock__:没有访问auto变量
void (^block1)(void) = ^{
NSLog(@"block1");
};
NSLog(@"%@", [block1 class]);
// 2. __NSStackBlock__:访问了auto变量
int age = 10;
void (^block2)(void) = ^{
NSLog(@"block2 - %d", age);
};
NSLog(@"%@", [block2 class]);
// 3. __NSMallocBlock__:__NSStackBlock__调用了copy
NSLog(@"%@", [[block2 copy] class]);
}
return 0;
}
// 打印结果
Demo[1234:567890] __NSGlobalBlock__
Demo[1234:567890] __NSStackBlock__
Demo[1234:567890] __NSMallocBlock__
输出打印结果可知block的三种类型:
上述代码转化为C++去查看源码发现三个block的isa
指针全部都指向_NSConcreteStackBlock
类型地址。原因是runtime会对其类型进行了转变,所以以runtime运行时类型即打印出的类型为准。
总结可得:
block类型 | 内存区域 | 环境 | 复制效果copy
|
---|---|---|---|
NSGlobalBlock |
数据段 | 没有访问auto 变量 |
什么也不做,类型不变 |
NSStackBlock |
栈 | 访问了auto 变量 |
从栈复制到堆,类型改变为__ NSMallocBlock__
|
NSMallocBlock |
堆 |
__ NSStackBlock__ 调用copy
|
引用计数增加,类型不变 |
栈中的__NSStackBlock__
访问auto
变量,作用域执行完毕被回收,如果调用block时已经销毁其内存,就会出现问题:
void (^block)(void);
void test() {
// __NSStackBlock__:访问了auto变量
int age = 10;
block = ^{
NSLog(@"age = %d", age);
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block();
}
return 0;
}
// 打印结果
Demo[1234:567890] age = -272632568
由打印结果可知:
age
没有打印出正确结果,这是因为__NSStackBlock__
类型的block存储在栈中,test
函数执行完毕后,栈内存中block所占用的内存被系统回收。
我们可以通过copy
将NSStackBlock
类型的block转化为NSMallocBlock
类型的block
void (^block)(void);
void test() {
// __NSStackBlock__ 调用copy转化为 __NSMallocBlock__
int age = 10;
block = [^{
NSLog(@"age = %d", age);
} copy];
[block release];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block();
}
return 0;
}
// 打印结果
Demo[1234:567890] age = 10
- MRC环境下,经常使用
copy
将栈上的block拷贝到堆中,然后手动调用release
操作将其销毁即可;- ARC环境下,编译器会自动将栈上的block进行
copy
操作,将block复制到堆上。
4. ARC帮我们做了什么?
在ARC环境下,编译器会根据情况自动将栈上的block进行一次
copy
操作,将block复制到堆上:
- block作为函数返回值时;
- block赋值给
__strong
指针时;- block作为Cocoa API中方法名含有
usingBlock
的方法参数时;- block作为GCD API的方法参数时。
(1) block作为函数返回值时:
typedef void(^MyBlock)(void);
MyBlock myblock() {
int age = 10;
return ^{
NSLog(@"-------- %d", age);
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// block作为函数返回值
MyBlock block = myblock();
block();
// MRC打印:__NSStackBlock__ (访问了auto变量)
// ARC打印:__NSMallocBlock__
NSLog(@"%@", [block class]);
}
return 0;
}
block访问auto
变量时,block的类型为__NSStackBlock__
;
ARC将栈上的block进行一次copy
操作,将block复制到堆上,并在适当的地方进行release
操作,所以ARC打印block为__NSMallocBlock__
类型。
(2) 将block赋值给__strong指针时:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// block内没有访问auto变量
MyBlock block = ^{
NSLog(@"block---------");
};
NSLog(@"%@", [block class]);
// block内访问了auto变量,但没有赋值给__strong指针
int age = 10;
NSLog(@"%@", [^{
NSLog(@"block1--------- %d", age);
} class]);
// block赋值给__strong指针
MyBlock block2 = ^{
NSLog(@"block2--------- %d", age);
};
NSLog(@"%@", [block2 class]);
}
return 0;
}
由打印结果可知:
将block赋值给__strong
指针时,RAC会自动进行一次copy
操作。
(3) block作为Cocoa API中方法名含有usingBlock
的方法参数时:
例如:遍历数组的block方法,将block作为参数
NSArray *array = @[];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
(4) block作为GCD API的方法参数时:
例如:GCD的一次性函数或延迟执行的函数
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
});
MRC下block属性的建议写法:
@property (copy, nonatomic) void (^block)(void);
ARC下block属性的建议写法:
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);