1、前言
对于初中级iOS开发工程师来说,面试的时候手写block是比较常见的问题,那对于高级及以上在问到block的使用的时候,不得不提block的变量截获本质了。在此我对此问题做一些总结,仅供各位大佬借鉴、斧正。
2、实例探究
在开始之前,我们先来总结一下变量的几种类型:
a.局部变量(基本数据类型)
b.局部变量(对象类型)
c.静态局部变量
d.全局变量和静态全局变量
下面开始我们一一分析;
a.局部变量 -- 基本数据类型
来看下面一段代码:
- (void)viewDidLoad {
[super viewDidLoad];
int a = 10;
void (^varBlock)(void) = ^{
NSLog(@"%d", a); // 只会打印10
};
a = 20;
varBlock();
}
然后我们使用clang命令clang -rewrite-objc ViewController.m
来观察,编译后的ViewController.cpp文件代码。
注:
如果上述命令报错fatal error: 'UIKit/UIKit.h' file not found
,可换成clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m
我们会在文件中找到以下代码:
__ViewController__viewDidLoad_block_impl_0
方法,为viewDidLoad编译后的完整方法,在此可以看到我们定义的varBlock编译为(后续重点关注的结构体):
struct __ViewController__viewDidLoad_block_impl_0 {
struct __block_impl impl;
struct __ViewController__viewDidLoad_block_desc_0* Desc;
int a;
__ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
我们可以看下__block_impl
这个结构体:
struct __block_impl {
void *isa; // 指向class的指针
int Flags; // 标识
int Reserved; // 保留的变量
void *FuncPtr; // 函数指针
};
所以我们可以说Block是将 函数 及其 执行上下文 封装起来的对象
。上述的字段就不一一介绍了,感兴趣的可以留言讨论。
还记得我刚刚提到的重点么,可以看到最后截获的变量int a
就是已经作为参数传递进去了,并且是截获的是变量a
的值,所以后续无论怎么修改,我们block中的变量a
的值,都是我们在定义时已经传递进去的值。也就是10。
补充:
接下来在这个地方补充一个面试比较常问的问题,也就是修改变量a
的值,我们需要在block的声明之前把int a = 10;
修改为__block int a = 10;
,下面来看一下__block
修饰后的,变量截获的问题,同样的使用clang编译文件,看一下重点代码:
int a
变成了__Block_byref_a_0 *a
,我们在看看__Block_byref_a_0
这个是什么东西:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
找到了,原来__Block_byref_a_0
也是一个含有isa
的结构体,也就是说,用__block
修饰后,变量a
变成了一个对象,并且把对象的地址传递给了block,所以可以在block中修改变量的值。
- (void)viewDidLoad {
[super viewDidLoad];
__block int a = 10;
void (^varBlock)(void) = ^{
NSLog(@"%d", a); // 打印结果为20
};
a = 20;
varBlock();
}
b.局部变量 -- 对象类型
测试代码:
- (void)viewDidLoad {
[super viewDidLoad];
__strong NSNumber *a = @10;
void (^varBlock)(void) = ^{
NSLog(@"%@", a); // a = 10
};
varBlock();
}
我们看下上述代码的编译结果:
NSNumber *a;
所以我们接着往下看__attribute__((objc_ownership(strong))) NSNumber *a = ...
这段代码可以看到其修饰符为strong。我们修改strong为__unsafe_unretained
来看下结果:__attribute__((objc_ownership(none))) NSNumber *a = ...
,由此,我们可以得出结论:对于对象类型的局部变量是连同其修饰符
一起截获的。因此,在这里截获了对象的修饰符,所以强引用对象,在使用的时候可能为造成循环引用
,导致内存泄漏。
c.静态局部变量
测试代码:
- (void)viewDidLoad {
[super viewDidLoad];
static NSNumber *a;
static int b = 20;
void (^varBlock)(void) = ^{
NSLog(@"%@ -- %d", a, b); // 10 -- 30
};
a = @10;
b = 30;
varBlock();
}
编译结果如下图:
以指针的形式截取的
。
d.全局变量和静态全局变量
测试代码:
@implementation ViewController
// 全局变量
int global_a = 10;
// 静态全局变量
static int static_global_a = 20;
- (void)viewDidLoad {
[super viewDidLoad];
void (^varBlock)(void) = ^{
NSLog(@"%d -- %d", global_a, static_global_a);
};
varBlock();
}
@end
编译结果如下图:
不截获
。
3、总结
最后,祝大家早日成为大牛。欢迎留言交流。