Block详解
窥探block底层结构
我们写下一个最简单的block使用clang指令生成对应的C\C++代码
void (^block)(void) = ^{
NSLog(@"Hello, World!");
};
block();
截取关键代码如下
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;
// 构造函数(类似于OC的init方法),返回结构体对象
__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执行逻辑的函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
//这一句就是打印"Hello, World!"
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_c60393_mi_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)};
// 定义block变量
void (*block)(void) = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA
);
// 执行block内部的代码
block->FuncPtr(block);
从上面代码可以看出,block本质上也是一个OC对象,内部也有个isa指针,并且内部封装了函数调用。
block的变量捕获
写下一个访问外部变量的block
int age = 10;
void (^block)(void) = ^{
NSLog(@"age is %d",age);
};
block();
生成C\C++代码,截取关键部分
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
};
//block内部的函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2w_5f0_0k152c5d825xmdf9ztmr0000gn_T_main_b9df52_mi_0,age);
}
int age = 10;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
我们可以观察到block结构体多了个age变量,并且在初始化block时,将外部的age变量赋值给了结构体内部这个age变量,当函数执行时,直接打印的是结构体内部的age变量。
所以我们可以总结一下,block就是封装了函数调用以及函数调用环境的OC对象
为了保证block内部能正常访问外部的变量,block有个变量捕获机制
变量类型 | 是否捕获 | 访问方式 |
---|---|---|
auto局部变量 | 是 | 值传递 |
static局部变量 | 是 | 指针传递 |
全局变量 | 否 | 直接访问 |
局部变量不写修饰默认就是auto变量,其实从内存上也很好理解block的捕获机制。auto局部变量在栈区,函数调用完后资源会被释放掉,而static局部变量是在程序运行过程中一直存在的(存在数据段),所以用指针随时可以找到,而全局变量本来就是在哪都可访问,根本没必要捕获。
block的类型
block有三种类型,可以通过class方法或者isa指针查看具体的类型,它们最终都继承自NSBlock
类型 | 判断依据 | 存储区域 | 调用copy结果 |
---|---|---|---|
__NSGlobalBlock__ | 没有访问auto变量 | 数据段 | 什么也不做 |
__NSStackBlock__ | 访问了auto变量 | 栈区 | 从栈区复制到堆 |
__NSMallockBlock__ | __NSStackBlock__调用了copy | 堆区 | 引用计数增加 |
在ARC环境下会根据情况自动将栈上的block复制到堆上,比如以下情况
- block作为函数返回值时
- block赋值给__strong指针时(对象类型的默认修饰就是__strong)
- block作为Cocoa API中方法名含有usingBlock参数时
- block作为GCD API的方法参数时
MRC下建议用copy修饰block属性,ARC可以用strong和copy修饰block属性
block访问对象类型的auto变量
写下如下代码,用clang指令生成C\C++
NSObject *obj = [[NSObject alloc] init];
void (^block)(void) = ^{
NSLog(@"%@",obj);
};
block();
截取部分关键代码,可以看到
当block访问对象类型的auto变量时,内部多了copy函数和dispose函数
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);}
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};
//定义block代码
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, obj, 570425344));
- 当block在栈上时,不会对auto变量产生强引用
- 当block从栈上被拷贝到堆上时
会调用block内部的copy函数,copy函数会调用内部的_Block_object_assign函数,该函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用或弱引用 - 当block从堆上移除时
block会调用内部的dispose函数,dispose函数会调用内部的_Block_object_dispose函数,该函数会释放引用的auto变量(release)
__block修饰符
__block可以用于解决block内部无法修改auto变量问题,__block不能用来修饰全局变量,静态变量(static)
__block int age = 10;
^{
age = 30;
}();
NSLog(@"age is %d",age);
生成C\C++代码
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // by ref
}
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
//__block int age = 10;
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
((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_2w_5f0_0k152c5d825xmdf9ztmr0000gn_T_main_ad80c7_mi_0,(age.__forwarding->age));
当我们使用__block修饰age变量时,会将age变量包转成age对象,age对象里的int age存储着最初的age值,__forwarding指针是指向age对象自己,block捕获的是age对象的地址值。从最后的一句可以看出,当我们使用__block修饰auto变量后,访问age都变成了访问age对象里的age成员变量。
- __forwarding指针
当block从栈上拷贝到堆上时,栈上对象的__forwarding会指向堆上的拷贝对象(block拷贝到堆上时,会将捕获的对象变量一并copy到堆上)
block循环引用问题
从上面我们可以看到,block访问对象类型的auto变量时有可能会产生强引用,当访问的auto变量又对block产生强引用时就会发生循环应用。举例如下
typedef void(^MyBlock)(void);
@interface Person : NSObject
@property (nonatomic, copy) MyBlock myBlock;
@end
//main函数里面
Person *person = [[Person alloc] init];
person.myBlock = ^{
NSLog(@"%@",person);
};
在ARC环境下可以使用__weak、__unsafe_unretained解决(一般使用__weak,会自动置nil)
Person *person = [[Person alloc] init];
//或者 __unsafe_unretained typeof(Person *) weakPerson = person;
__weak typeof(Person *) weakPerson = person;
person.myBlock = ^{
NSLog(@"%@",weakPerson);
};
在MRC环境下可以使用__unsafe_unretained解决