Block底层本质
- block就是Objective-C对闭包的实现,闭包就是一个没有名字的函数或者指向函数的指针。block本质上也是一个OC对象,它内部有isa指针;
- block是封装了函数调用以及函数调用环境(参数)的OC对象;
我们来看一段代码
#import
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
void(^block)(int, int) = ^(int b,int c){
NSLog(@"%d",a);
NSLog(@"Hello World!");
};
block(10,10);
}
return 0;
}
把上面这段代码转化为C++底层语言,转化后:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int a = 10;
//定义block变量
void(*block)(int, int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
//执行block内部的代码
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 10);
}
return 0;
}
在C++代码中,block代码块底层调用__main_block_impl_0
。这句代码调用以下代码
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;
}
};
我们发现,block的底层也是一个结构体。搜索struct __block_impl
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
block的第一个结构体成员是一个isa指针。这说明,block也是一个OC对象。
__main_block_desc_0
结构体成员包括两个参数,如下:
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)};
__main_block_func_0
函数内部封装了block执行逻辑的函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int b, int c) {
int a = __cself->a; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2n_ksb7n0n131n2y7v9xcf411fm0000gn_T_main_7cbb05_mi_0,a);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2n_ksb7n0n131n2y7v9xcf411fm0000gn_T_main_7cbb05_mi_1);
}
在struct __block_impl
结构体内,有一个成员void *FuncPtr
,
Block变量捕获
为了保证block内部能够正常访问外部的变量,block有个变量捕获机制。
先来看一段代码
#import
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
void (^block)(void) = ^{
NSLog(@"%d",a);
};
a = 20;
block();
}
return 0;
}
执行上面这段代码,打印值是10,而不是20。之所以执行block()
,结果是10,而不是20,这个就使用了变量捕获
。
我们来看下C++底层代码
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int a = 10;
void (*block)(void) = ((void (*)())&__main_block_impl_0(
(void *)__main_block_func_0,
&__main_block_desc_0_DATA,
a));
a = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
在上面的代码中,编译时,在block
内部已经捕获到a
值。然后传递到__main_block_impl_0
。
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;
}
};
在上面代码中,根据传递过来的值,赋值给NSLog(@"%d",a);
。当a = 20;
时,仅仅是改变int a = 10;
的值。而block
内部获取不到a
的值。
-
int a = 10;
默认是auto
修饰,是值传递;auto,自动变量,auto修饰的变量,内存会自动消失。所以,block内部不会捕获会自动消失的内存。 - 如果
int a = 10;
使用static
修饰,则传递的是地址,block
内部捕获到变量的地址,如果在外部修改变量的值,则根据地址找到变量存储的值。 - 全局变量并不会被捕获到
block
内部。在block
内部会直接访问全部变量。 -
self
是一个局部变量,在block
内部,也会捕获self
。
Block类型
因为block
是一个对象,所以block
也是有类型的。block
有三种类型,可以通过class
方法或者isa
指针查看具体类型,最终都是继承自NSBlock
类型。
-
NSGlobalBlock
存储在数据区域,没有访问auto。 -
NSStackBlock
存储在栈区,会自动销毁,访问了auto。 -
NSMallocBlock
存储在堆区,需要程序员销毁,NSStackBlock
调用了copy
(ARC环境下,block会自动调用copy,从栈上赋值到堆上,所以一般block类型是NSMallocBlock
)。
ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况:
block作为函数返回值时;
将block赋值给__strong指针时;
block作为Cocoa API中方法名含有usingBlock的方法参数时;
block作为GCD API的方法参数时。
MRC下block属性的建议写法
@property (copy,nonatomic) void (^block)(void);ARC下block属性的建议写法
@property (copy,nonatomic) void (^block)(void);
@property (strong,nonatomic) void (^block)(void);
对象类型的auto变量以及Block的内存管理
类似于局部变量,有auto修饰的对象在block内部,也会存在block类型。来看一段代码
#import
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end
#import "Person.h"
@implementation Person
- (void)dealloc{
NSLog(@"delloc--Person");
}
@end
main.m
#import
#import "Person.h"
typedef void(^HYBlock) (void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
HYBlock myBlock;
{
Person *p = [[Person alloc] init];
p.age = 10;
myBlock = ^{
NSLog(@"%d",p.age);
};
myBlock();
}
}
return 0;
}
当block内部访问了对象类型的auto变量时
如果block是在栈上,将不会对auto变量产生强引用。当block被拷贝到堆上
✔️会调用block内部的copy函数;
✔️copy函数内部会调用_Block_object_assign
;
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
✔️_Block_object_assign
函数会根据auto变量的修饰符(__strong、__weak、__unsafe unretained)做出相应的操作,形成强引用(retain)或者弱引用。
- 如果block从堆上移除
✔️会调用blokc内部的dispose函数
✔️dispose函数内部会调用_Block_object_dispose
函数
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
✔️_Block_object_dispose
函数会自动释放引用的auto变量。
Block修饰符
我们知道不能再block内部修改外部变量的值,我们来看下原因:
#import
typedef void(^HBlock) (void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
HBlock block= ^{
NSLog(@"%d",a);
};
block();
}
return 0;
}
转化为C++代码
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int a = 10;
HBlock block= ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
在上面的代码中,定义了int a = 10;
。而输出这个变量值是在下面这个函数中
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2n_ksb7n0n131n2y7v9xcf411fm0000gn_T_main_5b923f_mi_0,a);
}
因为变量a
不是全局变量,只是局部变量,所以不能在另外一个函数,修改变量值。
- 如果
int a = 10;
使用static
修饰,可以在block内部修改变量的值。
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;
}
};
在上面的代码中,int *a;
传递的是变量的地址值,在block内部,先找到变量的地址值,直接修改变量a的值,而不是直接修改变量值。
- 如果
int a = 10;
是全局变量,则在当前文件的函数中,都可以修改变量值。 - 使用
static
修饰变量,或者使用全局变量,则这个变量一直在内存中。如果使用__block
修改,也可以在block内部修改变量值,并且,变量会自动释放,不会一直存在内存中。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
...
};
从上面的代码可以看出,使用__block
修饰变量,在__main_block_impl_0
内部,变量a为__Block_byref_a_0 *a; // by ref
。而__Block_byref_a_0
是一个对象(内部有isa指针)。在这个结构体内部,有成员变量,存储变量值。
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
当修改变量值时,利用__Block_byref_a_0
指针先找到结构体,通过变量名找到__forwarding
,在通过__forwarding
找到变量,来修改变量值。
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
(a->__forwarding->a) = 20;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2n_ksb7n0n131n2y7v9xcf411fm0000gn_T_main_b8c75c_mi_0,(a->__forwarding->a));
}
- 注意
✔️使用__block修饰int a
,则在block内部a
成为对象。
✔️创建NSMutableArray *array = [NSMutableArray array];
,在block内部使用[array addObject:@123]
是使用这个指针,而不是改变array的值。
block循环引用
循环引用是指两个或以上对象互相强引用,导致所有对象无法释放的现象。这是内存泄露的一种情况。
#import
typedef void(^HYBlock)(void);
@interface Person : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, copy) HYBlock block;
@end
#import "Person.h"
@implementation Person
- (void)dealloc{
NSLog(@"%s",__func__);
}
@end
#import
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.age = 20;
person.block = ^{
NSLog(@"%d", person.age);
};
person.block();
}
return 0;
}
在上面的代码中,当执行person.block();
时,Person对象并没有释放,产生循环引用。
我们来看下,产生循环引用的原因,首先转化为C++代码
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__strong person;
...
};
在ARC环境下,HYBlock
被拷贝到堆上,当内部调用person
时,则在函数__main_block_impl_0
内部,Person
对象生成Person *__strong person;
也即是强引用这个对象。Person对象强引用HYBlock
,HYBlock
又强引用Person对象,则HYBlock
不释放,Person
对象也不会释放。
解决循环引用
- 使用__weak,__unsafe_unretained解决;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__weak weakPerson;
...
};
使用weak修饰对象,则在函数__main_block_impl_0
内部,不在强引用Person对象。__unsafe_unretained同理,也不在强引用Person对象。
- 使用__block解决(必须调用block);
#import
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block Person *person = [[Person alloc] init];
person.age = 20;
person.block = ^{
NSLog(@"%d", person.age);
person = nil;
};
person.block();
}
return 0;
}