1. Block的基本结构
void (^testBlock)(void) = ^{
NSLog(@"臭吉吉~");
};
testBlock();
将包含Block的代码通过clang转换为c++代码(只用了c++的扩展struct,实际上还是c)。我们一句一句看:
- Block变量的声明:
void (*testBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
可以看到,testBlock变量,实际上是 __main_block_impl_0 结构体实例的指针。
__main_block_impl_0的结构为:
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_impl 结构体中声明:
struct __block_impl {
// 类型
void *isa;
// 引用计数等会存在这里
int Flags;
// 保留位
int Reserved;
// 函数指针
void *FuncPtr;
};
其中,FuncPtr指向的就是我们在Block中提供的函数体。而isa,即作为描述Block类型使用。由于Block在堆中也是遵循类似自动引用计数的内存管理机制,故可以把Block看做为对象。
而Block的描述信息,则是指向全局的 __main_block_desc_0 结构体的实例。
static struct __main_block_desc_0 {
// 保留位
size_t reserved;
// Block整体的内存占用
size_t Block_size;
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
- Block的执行
((void (*)(__block_impl *))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock);
了解了Block的结构,这一句就很好理解了。由于testBlock的地址与 __block_impl 指针的地址相同,因此直接转换为 __block_impl 类型。然后,获取其中的 FuncPtr 函数指针,传入自身作为参数后,直接调用执行。
传入自身作为 FuncPtr 的参数的目的:
由于Block的函数体在编译后成为了全局静态c函数(无状态保存)。因此,为了在调用时可以正常访问到捕获的变量,则将自身实例作为参数传入(这与OC调用方法的传参目的一样)。
2.Block捕获的变量
2.1 没有捕获变量
Block在没有捕获任何变量时,其类型(isa)为NSGlobalBlock。
2.2 捕获基本类型变量
测试代码:
NSInteger value = 3;
void (^testBlock)(void) = ^{
NSLog(@"%ld", value);
};
testBlock();
在运行时,此Block的类型为NSMallocBlock。已经被copy到堆中。
对于基本数据类型的变量,捕获后,其值直接保存到 __main_block_impl_0 结构体中:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 直接保存值
NSInteger value;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSInteger _value, int flags=0) : value(_value) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
由于是值传递,直接修改此Block变量中的value是不会影响原value的值。因此,编译器则直接不允许修改捕获的变量。
而且,这也解释了为何在 FuncPtr 中的需要传入block自身作为参数:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
// 通过自身取出捕获的变量
NSInteger value = __cself->value;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_zmhjsn0s5szgbxdxfqvbgqlc0000gn_T_main_cf26fa_mi_0, value);
}
2.3 捕获对象类型变量
测试代码:
id obj = [[NSObject alloc] init];
void (^testBlock)(void) = ^{
NSLog(@"%@", obj);
};
testBlock();
在运行时,此Block的类型为NSMallocBlock。已经被copy到堆中。
由于捕获的是对象类型,因此编译后的c++代码与刚才有些不同:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 直接保存对象(也就是地址)
id obj;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id _obj, int flags=0) : obj(_obj) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
核心结构还是一样,直接将捕获对象保存到了 __main_block_impl_0 结构体中。产生变化的,是 __main_block_desc_0 的结构:
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
// Block被copy时,捕获的变量执行的copy函数
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
// Block释放时,捕获的变量执行的释放函数
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
};
由于捕获的变量是对象类型,因此,需要在结构体中指定实现内存管理方式的相应实现(clang可以在Block的相关结构体中对OC对象进行内存管理,但需要提供相应实现)。
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*/);
}
也就是说,当Block变量被copy到堆上时,系统则会调用 _Block_object_assign 函数,对捕获的obj进行retain;而当堆上的Block变量被释放时,系统则会调用 _Block_object_dispose 函数,对捕获的obj进行release操作。
为了行为一致,编译器也不允许对捕获的对象类型变量进行修改。
这可以保证捕获的对象在超出自身作用域后,继续生存(因为已经被堆上的Block保留)。
2.4 捕获__block修饰的基本类型变量
测试代码:
__block NSInteger value = 3;
void (^testBlock)(void) = ^{
value -= 1;
};
testBlock();
NSLog(@"%ld", value);
首先,还是可以确认的是,在运行时,Block的类型是NSMallocBlock。
转换代码后,就可以看到,使用了 __block 修饰符的实现就变了很多。我们还是一句一句来看:
__attribute__((__blocks__(byref))) __Block_byref_value_0 value = {
(void*)0,
(__Block_byref_value_0 *)&value,
0,
sizeof(__Block_byref_value_0),
3
};
可以看到,__block 修饰的变量,实际上是一个全局的 __Block_byref_value_0 结构体的实例。我们看一下此结构体的内容:
struct __Block_byref_value_0 {
// 类型标识
void *__isa;
// 指向自身实例的指针
__Block_byref_value_0 *__forwarding;
int __flags;
int __size;
// 真正的值
NSInteger value;
};
可以看到,原始变量的真实值保存在结构体中。此结构体中不仅包含了类型标识、尺寸等信息,还包含了一个指向自身实例的指针。
下面是Block变量声明,只是将 __Block_byref_value_0 的地址传入,没有什么异常:
// testBlock变量声明及赋值
void (*testBlock)(void) = ((void (*)())&__main_block_impl_0(
(void *)__main_block_func_0,
&__main_block_desc_0_DATA,
(__Block_byref_value_0 *)&value,
570425344)
);
// __main_block_impl_0的结构体
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 引用传递捕获的变量
__Block_byref_value_0 *value;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_value_0 *_value, int flags=0) : value(_value->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到,唯一的区别就是,在捕获的带有 __block 修饰的变量,生成的Block变量中,是以引用传递的方式进行储存的。这也就意味着捕获的变量的内容是可以随意修改的,而且,访问或者修改的是 __Block_byref_value_0 的实例,而不是原始的变量。
对于Block中的描述信息,其实现也有些许变化:
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 修饰的变量,在捕获到Block中后,也需要在Block被copy到堆上、或从堆中释放时提供对应的内存管理函数。
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->value, (void*)src->value, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->value, 8/*BLOCK_FIELD_IS_BYREF*/);
}
这里与捕获对象类型变量时,生成的内存管理函数中,区别只是类型不同,是 BLOCK_FIELD_IS_BYREF (捕获的对象类型变量是 BLOCK_FIELD_IS_OBJECT )。
与对象的保留关系不同,这种方式,实际上是创建一个新对象(结构体实例,如 __Block_byref_value_0 ,内部包含着被捕获的变量的值)直接存储在Block中。当Block被copy到堆上时,再创建一个新的 __Block_byref_value_0 实例,并保存在堆上的Block中。
在 __Block_byref_value_0 的结构中,为什么会包含一个指向自身实例的指针 __forwarding ?
为了保证访问到捕获变量的一致性。
在Block被copy到堆上时,不仅生成一个新的 __Block_byref_value_0 实例。而且将原始 __Block_byref_value_0 的 __forwarding 指针指向了新的实例。因此,通过形如 value.__forwarding->value 的方式,不管是在栈上,还是在堆上,都可以访问到堆中的同一个变量。
所以,我们最后看一下在Block执行之后,打印语句NSLog。
NSLog(
(NSString *)&__NSConstantStringImpl__var_folders_z5_zmhjsn0s5szgbxdxfqvbgqlc0000gn_T_main_316d85_mi_0,
(value.__forwarding->value)
);
由于是在栈上执行,因此 value.__forwarding->value 最终指向的是堆上的Block中的新 __Block_byref_value_0 实例。
2.5 捕获__block修饰的对象类型变量
测试代码:
__block id obj = [[NSObject alloc] init];
void (^testBlock)(void) = ^{
obj = [[NSMutableArray alloc] init];
};
testBlock();
NSLog(@"%@", obj);
转换后的代码与 __block 修饰的基本类型变量很相似,都是生成一个对应的结构体实例,然后将变量存储在内部。
我们看一下生成过程(代码经过简化):
__attribute__((__blocks__(byref))) __Block_byref_obj_0 obj = {
(void*)0,
(__Block_byref_obj_0 *)&obj,
33554432,
sizeof(__Block_byref_obj_0),
__Block_byref_id_object_copy_131,
__Block_byref_id_object_dispose_131,
[[NSObject alloc] init]
};
其中,__Block_byref_obj_0 的结构如下所示:
struct __Block_byref_obj_0 {
void *__isa;
// 指向自身实例的指针
__Block_byref_obj_0 *__forwarding;
int __flags;
int __size;
// obj
void (*__Block_byref_id_object_copy)(void*, void*);
// obj释放函数
void (*__Block_byref_id_object_dispose)(void*);
// 真正的对象
id obj;
};
可以看到,__block 修饰的对象类型结构体,不仅包含与基本类型一样的成员,额外还包含了两个内存管理函数,用于在自身实例因Block的内存变化导致的变化时,包含的obj进行的保留和释放操作(Block的内存管理 -> __Block_byref_obj_0的内存变化 -> obj的内存变化)。
这里,我们看一下这一对内存管理函数的简单实现:
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
_Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}
可以看到,以copy方法为例,实际上与描述信息 __main_block_desc_0_DATA 中的 __main_block_copy_0 函数实现一样,都是调用了 _Block_object_assign 函数。只不过参数有些许不同:
src+40偏移量即为 __Block_byref_obj_0 结构体中的obj的地址。131即 BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT 。
以 _Block_object_assign 实现为例(节选自苹果的Blocks源代码 Blocks/Sources/runtime.c):
void _Block_object_assign(void *destAddr, const void *object, const int flags) {
...
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
/*******
// copy the actual field held in the __block container
__block id object;
__block void (^object)(void);
[^{ object; } copy];
********/
// under manual retain release __block object/block variables are dangling
_Block_assign((void *)object, destAddr);
break;
...
}
}
static void (*_Block_assign)(void *value, void **destptr) = _Block_assign_default;
static void _Block_assign_default(void *value, void **destptr) {
*destptr = value;
}
可以看到,在这种情况下,copy操作只是使用一个新的指针指向原始obj。
在ARC下,实际上就是对obj进行了强引用,也就是retain操作;但是在非ARC下,这只是一个指针指向,可能造成悬垂指针访问,切记。
而在 __main_block_desc_0_DATA 中,使用的copy和dispose函数与 __block 修饰的基本类型变量一致:
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, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);
}
最后,再看一下我们在Block函数体中对捕获变量的修改(代码已简化):
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
// 通过引用访问
__Block_byref_obj_0 *obj = __cself->obj;
// 通过__forwarding指针访问到的永远是相同的obj
(obj->__forwarding->obj) = [[NSMutableArray alloc] init];
}
3. 总结
- 在ARC环境下,Block在不捕获变量时,是NSGlobalBlock类型;否则,都是NSMallocBlock类型。
- 捕获到的基本数据类型变量或OC对象,直接存储值到Block的数据结构中,为值传递,外部修改无效。
- 捕获到的__block修饰的基本类型变量或OC对象,是以包装成的新的结构体实例的方式存储到Block的数据结构中,为引用传递,可以进行修改。
- ARC环境下,Block从栈上到被copy到堆上时,捕获的OC对象或是__block的OC对象,都会被retain;捕获的__block的基本类型变量,会创建一个新的结构体,保存在copy后的Block中。
- 非ARC环境下,使用__block修饰的OC对象,在被Block捕获后,可以防止循环引用(只是指针指向,没有retain操作,ARC下才是默认retain)。在ARC下,使用__weak修饰变量替代。