block是经常使用的一种技术,那么block的本质是什么呢?
Block的本质
block本质上也是OC对象,它内部也有isa指针
block是封装了函数调用已经函数调用环境的OC对象
我们创建一个命令行项目,在main函数中创建一个block
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^blockMe)(int) = ^(int a) {
NSLog(@"%d", a);
};
blockMe(10);
}
return 0;
}
使用命令将main.m转换成C/C++代码
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
在main.mm中
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
……
};
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
}
__main_block_impl_0这个便是block的底层结构,可以看到它内部确实有isa指针,所以它是一个OC对象
block的变量捕获
我们更新main函数中的代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
void (^blockMe)() = ^ {
NSLog(@"%d", a);
};
a = 20;
blockMe();
}
return 0;
}
很明显,运行后会打印10
那么为什么会打印10呢?
我们把main函数转换成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;
}
};
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
}
可以看到__main_block_impl_0内部多了一个参数int a
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int a = 10;
void (*blockMe)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
a = 20;
((void (*)(__block_impl *))((__block_impl *)blockMe)->FuncPtr)((__block_impl *)blockMe);
}
return 0;
}
删掉一些强制转换,精简下代码如下
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int a = 10;
void (*blockMe)(void) = &__main_block_impl_0(__main_block_func_0,
&__main_block_desc_0_DATA,
a);
a = 20;
blockMe->FuncPtr(blockMe);
}
return 0;
}
可以看到在初始化block的时候,把a(值为10)传入到了block的构造函数里,并把值保存在block内部的int a变量里
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_0w_qpvfdl7j7c3brrgyj3tbv3xm0000gn_T_main_d13ce6_mi_0, a);
}
在调用block的时候,是从block内部的int a获取值,然后打印,所以不管外部a变成多少,都只会打印10
block对变量的捕获
auto变量就是我们平时写的局部变量,编译器会自动帮我们在前面加上auto修饰符
int a = 10;
//等价于
auto int a = 10;
我们来验证下这三个变量的捕获情况,更新main函数代码如下
int c = 30;
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
static int b = 20;
void (^blockMe)(void) = ^ {
NSLog(@"%d", a);
NSLog(@"%d", b);
NSLog(@"%d", c);
};
blockMe();
}
return 0;
}
将main.m文件转换成c++代码,在main.cpp文件里block的底层实现如下
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
int *b;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int *_b, int flags=0) : a(_a), b(_b) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
int *b = __cself->b; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_0w_qpvfdl7j7c3brrgyj3tbv3xm0000gn_T_main_7c0631_mi_0, a);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_0w_qpvfdl7j7c3brrgyj3tbv3xm0000gn_T_main_7c0631_mi_1, (*b));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_0w_qpvfdl7j7c3brrgyj3tbv3xm0000gn_T_main_7c0631_mi_2, c);
}
可以看到block内部存了a的值,b的地址,c没有捕获,在调用打印函数的时候,a是直接从内部访问值,b则是获取地址访问,c是直接访问
block捕获self
我们新建一个ZJPerson类
@interface ZJPerson : NSObject
@property (nonatomic, copy) void (^blockMe)(void);
- (void)test;
@end
@implementation ZJPerson
-(void)dealloc {
NSLog(@"123");
}
- (void)test {
self.blockMe = ^{
NSLog(@"%@", self);
};
self.blockMe();
}
@end
这个类在使用完销毁的时候会打印123,我们在main函数中使用ZJPerson类
int main(int argc, const char * argv[]) {
@autoreleasepool {
ZJPerson *person = [[ZJPerson alloc]init];
[person test];
}
return 0;
}
按理说,在main函数完成的时候,person会被释放掉,从而打印123日志,但是我们运行项目之后却没有输出
这是什么原因导致的person对象没有释放呢?我们把ZJPerson.m转换成cpp看下
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ZJPerson.m
在ZJPerson.cpp中搜索__ZJPerson__test_block_impl_0
struct __ZJPerson__test_block_impl_0 {
struct __block_impl impl;
struct __ZJPerson__test_block_desc_0* Desc;
ZJPerson *self;
__ZJPerson__test_block_impl_0(void *fp, struct __ZJPerson__test_block_desc_0 *desc, ZJPerson *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看见block将self捕获到了内部去,为什么会捕获self呢?我们来看看test的底层实现
static void _I_ZJPerson_test(ZJPerson * self, SEL _cmd) {
((void (*)(id, SEL, void (^ _Nonnull)()))(void *)objc_msgSend)((id)self, sel_registerName("setBlockMe:"), ((void (*)())&__ZJPerson__test_block_impl_0((void *)__ZJPerson__test_block_func_0, &__ZJPerson__test_block_desc_0_DATA, self, 570425344)));
((void (*(*)(id, SEL))())(void *)objc_msgSend)((id)self, sel_registerName("blockMe"))();
}
在test函数中,self被当作局部变量参数传了进去,依据前面的规则,局部变量会被block捕获进内部,所以self就被block捕获了。
这就造成了self持有block,block持有self的循环引用,从而导致person对象没有释放
block的类型
在我们用代码验证这个类型之前,我们需要把项目的环境从ARC调整为MRC
然后在main函数中,申明三个block对象,block1没有访问auto变量,block2访问了auto变量,block3为block2进行了copy操作
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^block1)(void) = ^{
NSLog(@"123");
};
NSLog(@"block1 is %@", [block1 class]);
int a = 10;
void(^block2)(void) = ^{
NSLog(@"%d", a);
};
NSLog(@"block2 is %@", [block2 class]);
void(^block3)(void) = [block2 copy];
NSLog(@"block3 is %@", [block3 class]);
}
return 0;
}
运行之后结果如下
block对对象类型的auto变量的捕获
- 如果block在栈上,则不会对auto变量进行强引用
我们在MRC环境下,更新main函数和ZJPerson
类的代码如下
@interface ZJPerson : NSObject
@property (nonatomic, assign) int age;
@end
@implementation ZJPerson
-(void)dealloc {
[super dealloc];
NSLog(@"ZJPerson dealloc");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^stackBlock)(void);
{
ZJPerson *person = [[ZJPerson alloc]init];
person.age = 10;
stackBlock = ^{
NSLog(@"----%d", person.age);
};
[person release];
}
NSLog(@"%@", [stackBlock class]);
NSLog(@"1111111");
}
return 0;
}
并且在NSLog(@"1111111");
处打上断点
按照之前捕获auto变量的逻辑来分析,在打断点的地方,person对象并不会dealloc,我们运行看看情况
可以看到person对象已经释放了,说明栈上的block不会对auto变量产生强引用
- block被copy到了堆上
- 会自动调用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变量的修饰符(__strong, __weak, __unsafe_unretained)来决定是否释放变量,类似于release
block内部访问了auto变量之后的底层代码
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
……
};
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}
__block修饰符
我们知道在block内部中是不可以修改外部auto变量的值的,如果想要改值的话需要给外部变量加上__block修饰符
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 10;
void(^blockMe)(void) = ^{
age = 20;
NSLog(@"%d", age);
};
blockMe();
}
return 0;
}
为什么不加__block就不能改变值呢?
首先age申明在main函数里是一个局部变量
当执行block里的代码时
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_0w_qpvfdl7j7c3brrgyj3tbv3xm0000gn_T_main_7cf10c_mi_0, age);
}
无法访问外部main函数里的age值,它只能改变捕获到内部的age的值,所以在block内部无法修改外部的auto变量
为什么加上__block就可以修改值了呢?我们把它转换成C++代码来看下
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // bound by ref
NSLog((NSString *)&__NSConstantStringImpl__var_folders_0w_qpvfdl7j7c3brrgyj3tbv3xm0000gn_T_main_110fcc_mi_0, (age->__forwarding->age));
}
可以看到,block的底层结构发生了变化,原本应该为int age
的成员变量变成了__Block_byref_age_0 *age
,而结构体__Block_byref_age_0
内部才有int age
成员变量
__block将修饰的auto变量包装成一个对象,比如
将int age
包装成__Block_byref_age_0
对象,
__Block_byref_age_0
内部持有int age
block对象持有__Block_byref_age_0
对象,
当执行block的代码的时候,先获取到__Block_byref_age_0
对象,然后再从__Block_byref_age_0
对象内部获取到int age
修改它的值
这样就解决了block内部访问不到外部auto变量的问题,从而达成可以修改外部变量的效果
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 10;
void(^blockMe)(void) = ^{
NSLog(@"%d", age);
};
//那这个age,访问的是哪个age呢?
//是__main_block_impl_0内部的__Block_byref_age_0 *age呢
//还是__Block_byref_age_0内部的int age
NSLog(@"%p", &age);
blockMe();
}
return 0;
}
为了解决这个问题,我们将blockMe用底层结构体的方式来实现一下,更新main函数的代码如下
struct __Block_byref_age_0 {
void *__isa;
struct __Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(void);
void (*dispose)(void);
};
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
struct __Block_byref_age_0 *age; // by ref
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 10;
void(^blockMe)(void) = ^{
NSLog(@"%d", age);
};
//那这个age,访问的是哪个age呢?
//是__main_block_impl_0内部的__Block_byref_age_0 *age呢
//还是__Block_byref_age_0内部的int age
struct __main_block_impl_0 *blockImpl = (struct __main_block_impl_0 *)blockMe;
NSLog(@"%p", &age);
blockMe();
}
return 0;
}
然后再blockMe();
处打上断点
然后运行代码,控制台打印如下
可以看到,我们在外部访问的age的地址为
0x00007ffeefbff558
, 我们在控制台输入如下命令
p/x &(blockImpl->age->age)
这个命令是查看blockMe的底层结构
__main_block_impl_0
内部的__Block_byref_age_0
内部的int age
的地址,其输出如下
由此可以得出结论
访问的是是
__Block_byref_age_0
内部的int age
__block中的__forwarding指针
我们可以看到用__block修饰的int age
,其底层结构如下
struct __Block_byref_age_0 {
void *__isa;
struct __Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
这个__forwarding指针是啥呢?
这个指针其实是指向它自己的
我们在main函数的底层代码中找到这一段代码
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
void(*blockMe)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_0w_qpvfdl7j7c3brrgyj3tbv3xm0000gn_T_main_110fcc_mi_1, &(age.__forwarding->age));
((void (*)(__block_impl *))((__block_impl *)blockMe)->FuncPtr)((__block_impl *)blockMe);
}
return 0;
}
精简一下,删除冗余的代码,如下
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__Block_byref_age_0 age = {0,
(__Block_byref_age_0 *)&age,
0,
sizeof(__Block_byref_age_0),
10};
……
}
return 0;
}
可以看到第二个参数传入的就是自己的地址
那么这个__forwarding指针有什么用呢?
当block在栈区的时候,栈区block的__forwarding指针指向栈区的block,当栈区的block拷贝到堆上的时候,栈区的__forwarding指针指向堆区的block,如下所示
这样就可以保证捕获的变量都是保存在堆区