block的本质
- block本质上也是一个OC对象,他的内部有一个isa指针
- block是封装了函数调用以及函数调用环境(比如函数参数)的OC 对象
- block也是一个匿名函数
写一个简单的block demo验证分析一下block的本质:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
void(^myBlock)(void) = ^{
NSLog(@"this is a block");
};
myBlock();
NSLog(@"%@",myBlock);
}
return 0;
}
使用clang命令 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
编译成C++代码,找到C++代码最后我们可以看到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;
}
};
第一个属性__block_impl
又是一个结构体
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
这里isa
指针是指向了block的类型
,这个后面再说,在后面贴的C++代码中可以注意一下,都是block的类型。
我们这个简单的myBlock内部只有一句NSLog的打印,被封装在了__main_block_func_0
这个函数中
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_86_ljphgvys0hzg8hfxdtv5q5gm0000gn_T_main_26cca1_mi_0);
}
__main_block_func_0
的函数地址又作为__main_block_impl_0
构造函数的第一个参数fp,被赋值给impl
的FuncPtr
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
调用FuncPtr,也就是调用myBlock中封装的函数,并且把block本身作为参数传进去
,这里传本身进去也是有说法的,后面就会用到,接着往下看。
根据以上对源码的分析 ,也验证了我们最开始说的两点block的本质。
变量捕获
什么是变量捕获,block
为什么要捕获外部变量,其实这个现象很常见,只是这么描述出来感觉有点儿抽象,比如在上面demo中,在block内部添加使用self
,或者self.property
,那么block中使用到的值,就会被捕获到block内部,那这些变量是以什么样的形式又是如何被block捕获到的呢。下面我们就由浅到深的继续探究block是如何获取外部变量的。
局部变量(auto变量)
所谓auto
变量也叫自动变量,就是离开作用域(他所在的{}
)就会自动销毁,也就是局部变量,因为auto
可以省略,所以我们平时都可以直接定义局部变量,不需要在前面加上auto修饰符。
先看一段示例代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
void(^myBlock)(void) = ^{
NSLog(@"this is a block - %d ",age);
};
age = 20;
myBlock();
}
return 0;
}
那么请问这段代码的打印age的值是多少呢?有人觉得是20,有人觉得是10,这里肯定的告诉大家,或者大家手动写一写这段代码测试一下打印结果,其实打印结果是10
.还是用clang编译成C++代码一看究竟
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_86_ljphgvys0hzg8hfxdtv5q5gm0000gn_T_main_edbb92_mi_0,age);
}
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)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int age = 10;
void(*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
age = 20;
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
}
return 0;
}
同样的结构体,同样的调用方式,上面说过的内容就不重复了,主要看auto变量age是如何被block获取到的:
__main_block_impl_0
结构体中添加了一个age
的成员变量,构造函数中也添加了age
,: age(_age)
是C++语法,表示默认执行age = _age
void(*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
直接把age的值获取到,并且保存在结构体内部,因为是值传递
,并没有捕获到age变量的内存地址__main_block_func_0
中通过参数__cself
,也就是上面提过的把block自身作为入参,然后通过__cself
取出age
age = 20;
虽然修改了age的值,但是block捕获的age的值并不会发生改变
基于这一点,在编译时,就抛出了auto变量无法在block内部被修改的错误
static变量
与auto
相对的,还有static
修饰的变量,那么block
对static
变量捕获的方式和auto
变量有什么不同呢?看下面的测试代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
auto int age = 10;
static int height = 50;
void(^myBlock)(void) = ^{
NSLog(@"age is - %d ,height is %d",age, height);
};
age = 20;
height = 100;
myBlock();
}
return 0;
}
输出
block[87871:3465064] age is - 10 ,height is 100
很明显我们在block定义之后修改static变量的值,block内部是可以知道的,再看C++代码中两个变量被捕获到结构体中的区别
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
int *height;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
根据上面编译完的代码可以很明显看到变量age和height被block获取时的区别,height被结构体__main_block_impl_0
存储的是内存地址,所以block中可以修改height的值。
全局变量
继续根据测试代码和C++代码,分析block对变量的捕获
#import <Foundation/Foundation.h>
int age = 10;
static int height = 50;
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^myBlock)(void) = ^{
NSLog(@"this is a block - %d ,height is %d",age, height);
};
age = 20;
height = 100;
myBlock();
}
return 0;
}
C++代码:
#pragma clang assume_nonnull end
int age = 10;
static int height = 50;
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;
}
};
可以得出结论:
- 全局变量不会被捕获到block内部,不管在程序哪里都可以直接访问
- 为什么局部变量需要捕获呢?其实上面也说过了,是因为作用域的问题,最后block的调用,是在跨函数访问变量,如果不捕获到block内部,访问auto变量的时候已经释放了
self的捕获
其实self
的捕获,才是开发中经常遇到的情况,也是引起block循环引用的罪魁祸首
。当然self的捕获可以归到上面两类中,但还是想拿出来单独分析一下。也算是对上面两类变量分析结果的实践吧。
#import "Person.h"
@implementation Person
- (void)test_block {
void(^block)(void) = ^{
NSLog(@"this is a block - %p",self);
};
}
@end
先思考一下,这里
self
会被捕获到block中嘛,self在这里是一个auto变量还是static变量还是全局变量呢?先分析出是什么变量,下一步我们才能知道self是以什么形式被捕获到block中或者会不会被捕获到block中。
你知道答案了吗?
看看下面的C++代码:
struct __Person__test_block_block_impl_0 {
struct __block_impl impl;
struct __Person__test_block_block_desc_0* Desc;
Person *self;
__Person__test_block_block_impl_0(void *fp, struct __Person__test_block_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以得到结论,block是会捕获self
的,可是self是个局部变量嘛?这里又要提到method
的两个默认参数了self
和_cmd
,代码中的test方法被编译成_I_Person_test_block
,他有两个参数是self
和_cmd
,函数的参数就是局部变量
,然后被捕获到block中赋值给struct中的self
static void _I_Person_test_block(Person * self, SEL _cmd) {
void(*block)(void) = ((void (*)())&__Person__test_block_block_impl_0((void *)__Person__test_block_block_func_0, &__Person__test_block_block_desc_0_DATA, self, 570425344));
}
同理,如果是block中访问self的成员变量(self.name)
或者通过self调用别的方法([self method])
,也同样会捕获self到block中。