大家在面试的时候是不是经常遇到这样的面试题:
运行下面的代码,打印结果是是什么?为什么?
// 全局变量
NSString *name_ = @"张三";
- (void)testBlock {
int age = 18;
static int height = 180;
void (^myBlock)(void) = ^{
NSLog(@"myBlock:age=%d,height=%d,name=%@", age, height, name_);
};
age = 28;
height = 160;
name_ = @"李四";
myBlock();
}
这个面试题呢,也就是今天要主要说的内容:Block的值捕获。
那么,下面就针对,局部变量、静态变量、全局变量这三种变量,研究一下,block在底层到底是怎么捕获外部的局部变量的,还有全局变量到底有没有捕获呢?(其实,还有一个对象类型的变量,后面的章节会说到)
一,Block捕获外部局部变量
1. 查看打印结果
运行代码:
- (void)testBlock {
int age = 18;
static int height = 180;
void (^myBlock)(void) = ^{
NSLog(@"myBlock:age=%d,height=%d", age, height);
};
age = 28;
height = 160;
myBlock();
}
// myBlock:age=18,height=160
通过打印结果,发现,只有静态变量height的值变了,变量age的值没变。
这是为啥呢???
下面咋们看看clang编译后的c++代码,他俩到底有啥区别?
注意两个关键字 auto和static
- auto:自动变量,默认就是auto的,离开作用域就销毁
- static:静态变量,不销毁
int age;
// 等价于
auto int age;
2. clang编译
在终端通过编译ViewController.m文件:
& xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m
生成ViewController.cpp文件:
3. Block捕获的变量最终去了哪?
分析c++代码(c++代码就不粘贴了),我汇总了一个示意图,可以清晰的看出,变量值的传递过程,以及存储地方,如下图所示:
根据上图简单说明几点:
- oc中声明 int age; 是省略了auto变量的,c++代码就可以看出
- auto变量是一个值传递
- static变量是指针传递,因为对height做了取地址符&操作了,相当于将这个值对应的地址取出来,然后传到方法里面去;
- 可以看出,这个block就是一个指向结构体的指针;
- 通过一个返回结构体的函数,将这两个变量存储到了block指针指向的结构体内存中。
说明一下:
- 值传递,就好比,将一个房间里面的人,给传出去,你这个房间以后在不在我就不关心;
- 指针传递,就好比,将这个房间传出去,也就是说,以后我就可以通过这个房间来找最新的人。
4. 捕获的变量是如何使用的?
现在通过block已经将外部的局部变量,捕获到了block的内存里了。
那么,具体用到这个值的时候,是怎么取出来的呢?
还是看图,我已经将相关c++代码片段做了流程说明,如图所示:
简单说明下这个示意图:
- 执行block时,就会通过block指针找到所指向的结构体,在结构体里面找到FuncPtr这个函数地址;
- 通过FuncPtr这个函数地址,找到这个函数的实现;
- 将block自己作为参数,传入这个函数;
- 可以看出,block本质上就是一个指向结构体的指针,所以,就可以通过该指针找到结构体,然后从结构体里面取出变量。
二,Block捕获全局变量
1. 添加全局变量,查看打印结果
int age_ = 10;
static int height_ = 170;
- (void)testBlock {
NSLog(@"myBlock前:age_=%d,height_=%d", age_, height_);
void (^myBlock)(void) = ^{
NSLog(@"myBlock后:age_=%d,height_=%d", age_, height_);
};
age_ = 30;
height_ = 200;
myBlock();
}
// myBlock前:age_=10,height_=170
// myBlock后:age_=30,height_=200
发现两个全局变量的值都改变了。
那么,接下来,我们看看age_和height_是不是也被block捕获了??
2. clang编译
在终端通过编译ViewController.m文件:
& xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m
生成ViewController.cpp文件:
3. 分析全部变量到底去哪了?
1> 查看Block底层数据结构
如下图所示,全局变量并没有被存储在结构体里面,也就是说,block压根就没有去捕获全局变量。
那么,为啥全局变量没有被捕获,也会随着其改变而改变呢?
那是因为:
可以这么理解,全局变量的值都可以访问,内存也不会释放(存储在数据段),block实现里面使用的时候,就不用担心这个值随时会释放的问题,反正我随时都可以访问你的内存,为啥我block还要再存储呢,对不对。
然而,局部变量需要被捕获的原因就是:
在block底层,使用变量的时候,是跨函数调用的。如果是一个局部变量,就需要将变量值提前存到block的struct里面。局部变量中的静态变量static修饰的变量,在底层是以地址(指针)的方式存储的,所以在block实现时,会去变量地址里面找最新的值。
三,Block捕获对象类型的auto变量
1. 创建一个Person对象
// 声明
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end
// 实现
@implementation Person
-(void)dealloc {
NSLog(@"%s", __func__);
}
@end
2. 运行代码
- (void)viewDidLoad {
[super viewDidLoad];
{
Person *person = [[Person alloc]init];
person.age = 18;
}
NSLog(@"------");
}
打印结果是:
-[Person dealloc]
------
在分割符前就打印了dealloc,说明这个Person对象在超出{}作用域后,就会销毁释放。
那么,用block捕获Person对象的属性,Person对象还会释放吗??
2. 用block捕获Person对象的属性
- (void)viewDidLoad {
[super viewDidLoad];
void(^block)(void);
{
Person *person = [[Person alloc]init];
person.age = 18;
block = ^{
NSLog(@"%d", person.age);
};
}
block();
NSLog(@"block类型 = %@", [block class]);
NSLog(@"------");
}
打印结果
age = 18
block类型 = __NSMallocBlock__
------
-[Person dealloc]
通过打印结果发现,在分割符前并没有打印-[Person dealloc],说明这个Person对象在离开{}作用域后没有释放。这是为啥呢??
1> Block捕获了对象的属性
初始化的这个实例对象person,其实也是一个auto变量。当Block捕获了auto变量,Block会存储在堆区。此时,就算Person的作用域结束了,Person对象还是保存在Block底层结构体数据里面的。
说明堆空间的Block对Person是强引用的,只有Block销毁了,Person才会被销毁。
看看Person对象在Block内存中是以什么样的形式存储的。
clang编译ViewController.m文件
& xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m
编译后的代码如下所示,这是Block内部的数据结构,其中存储着Block捕获的Person对象。
2> MRC环境下,会提前释放Person吗?
上面的测试都是在ARC下进行的,如果是在MRC环境下,此时的Block是在栈区,那么就会打印-[Person dealloc]这句话。
说明栈空间的Block不会持有外面的对象的,不会保住Person的命(MRC下没有强引用的说法)。
3> __weak修饰对象,会提前释放Person吗?
- (void)viewDidLoad {
[super viewDidLoad];
void(^block)(void);
{
Person *person = [[Person alloc]init];
person.age = 18;
__weak Person *weakPerson = person;
block = ^{
NSLog(@"%d", weakPerson age);
};
}
block();
NSLog(@"block类型 = %@", [block class]);
NSLog(@"------");
}
打印结果是:
-[Person dealloc]
age = 0
block类型 = __NSMallocBlock__
------
通过打印结果就可知道,{}作用域一结束,Person就释放掉了。
说明,在对象被__weak修饰后,堆空间的Block对Person是弱引用的,Person会随着作用域结束而销毁。
用clang编译ViewController.m文件,
// 这个命令行支持ARC、指定运行时系统版本
& xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-9.0.0 ViewController.m
下面就是编译后的代码片段截图,从中可以看出,Block内部结构体在存储Perosn对象的时候,也是用弱引用存储的。
3. block除了捕获对象类型的auto变量,还干了啥?
我们已经知道了,block不管是捕获基本数据类型的变量,还是捕获对象类型的auto变量,都会存储在block内存中的。
但是,在block捕获对象类型的auto变量时,还发生了什么事呢??
如下图所示:
一句话概括:
在block捕获对象类型的auto变量时,block内存中还会生成两个函数:copy函数 和 dipose函数
那么block的内存分布就可以是这样的,如图所示:
从上图可以看出,block捕获一个对象类型的auto变量的话,在block内存中会多出两个函数
- copy函数
- dispose函数
那么,这两个函数到底是干啥的呢???
4. block内存中的copy函数和dipose函数是干啥的?
1>copy函数
请看示意图:
简单说明下:
- 当block被拷贝到堆上时,block会自动调用内存中的copy函数,然后找到_Block_object_assign函数;
- _Block_object_assign函数,会根据block内存中的auto变量person是什么类型的,会对person产生强引用或者弱引用。也可以这么理解,_Block_object_assign内部会对person进行引用计数的操作。
如果person是被__strong或没有修饰的,那么就是强引用,引用计数就会+1;
如果person是被__weak修饰的,那么就是弱引用,引用计数就不会变。
2> dispose函数
请看示意图:
简单说明下:
- 当block从堆区移除时,block会自动调用内存中的dispose函数,然后调用__Block_object_dispose函数;
- __Block_object_dispose函数会自动释放引用的auto变量,相当于release操作,也就是断开对person对象的引用,而person究竟是否被释放还是取决于person对象自己的引用计数。
四,Block变量捕获总结
1. block捕获基本数据类型的变量
为了保证block内部能正常访问外部变量,block有个变量捕获机制:
简言之就是:
- 只有局部变量才能被block捕获,全局变量不会被捕获;
- 局部变量 auto类型属于值传递,不会因为该值的改变,使得block实现里面的值也改变;
- 局部变量static类型属于指针传递,该值改变,会导致block实现里面的值也跟着改变。
2. block捕获对象类型的auto变量
- 如果block(匿名block)是在栈上,将不会对对象类型auto变量产生强引用,对象随着作用域销毁而销毁.
- 如果block被拷贝到了堆上:
a>. 当block被拷贝到堆上时,block会自动调用内存中的copy函数,然后找到_Block_object_assign函数;
b>. _Block_object_assign函数,会根据block内存中的auto变量person是什么类型的,会对person产生强引用或者弱引用。
也可以这么理解,_Block_object_assign内部会对person进行引用计数的操作。
如果person是被__strong修饰或没有修饰的,那么就是强引用,引用计数就会+1;
如果person是被__weak修饰的,那么就是弱引用,引用计数就不会变。
- 如果block从堆上移除
a> 当block从堆区移除时,block会自动调用内存中的dispose函数,然后调用__Block_object_dispose函数;
b> __Block_object_dispose函数会自动释放引用的auto变量,相当于release操作,也就是断开对person对象的引用,而person究竟是否被释放还是取决于person对象自己的引用计数。
- 被捕获的对象什么时候销毁,取决于强引用什么时候销毁,强引用销毁了,对象也就销毁了(前提是自己的引用计数为0)。
五,回答文章开头的面试题
运行下面的代码,打印结果是是什么?为什么?
// 全局变量
NSString *name_ = @"张三";
- (void)testBlock {
int age = 18;
static int height = 180;
void (^myBlock)(void) = ^{
NSLog(@"myBlock:age=%d,height=%d,name=%@", age, height, name_);
};
age = 28;
height = 160;
name_ = @"李四";
myBlock();
}
打印结果是:
myBlock:age=18,height=160,name=@"李四"
原因:
age和height属于局部变量,在block内部使用,会被block捕获,存储在block内存中。
因为,在block底层,使用变量的时候,是通过跨函数调用的。如果是一个局部变量,就需要将变量值提前存储到block的struct里面。
age默认使用auto变量类型修饰的,属于值传递,所以在block实现时,会直接从struct里面取出已经存储的值;
height使用staitc静态变量修饰的,属于指针传递,在block内存中是以地址的方式存储的,所以在block实现时,会先在struct里面找到这个变量的地址值,然后去变量地址找最新的值。name_属于全局变量,所以不会被block捕获。但是,正因为是一个全局变量,在哪都可以访问,所以,block外部改变后,内部也会访问到name_的改变值。