0.前言
日常开发中经常会用到 Block,但如果对它的底层实现没有深入地挖掘过,就不能算是真正掌握,本篇就来探究一下 Block 的底层实现原理。
1.举个 🌰
先来看一个例子,下边是一种简单的 block 使用场景: 无参数、无返回值的 block。
typedef void(^MyBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 30;
// 创建
MyBlock blk = ^{
NSLog(@"My age is %d .", age);
};
// 执行
blk();
}
return 0;
}
2.Block的实质
为了探究 Block 的本质,我们需要借助 clang 将含有 Block 语法的源代码转换成 C++ 代码。
2.1 Block 的底层结构
终端执行 $ clang -rewrite-objc main.m
命令,就可以将 main.m
文件编译生成 main.cpp
文件,这里截取了 main.cpp
文件中与 block 相关的代码,并添加了部分注释:
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_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
// 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 flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
// 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_hh_nkvzrckj32q41pptw9t27cvc0000gn_T_main_d9ff54_mi_0, age);
}
// main() 函数
int main(int argc, const char * argv[]) {
/* @autoreleasepool */
{
__AtAutoreleasePool __autoreleasepool;
int age = 30;
MyBlock blk = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}
return 0;
}
下面开始一步步讨论源码。从 main() 函数开始,关于自动释放池的代码不在此处讨论,先看一下 block 的创建过程:
MyBlock blk = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
// 简化后的代码:
MyBlock blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age);
我们注意到这里实际上有两个函数:__main_block_impl_0()
和 __main_block_func_0()
。
先来看后者,具体代码如下,实际是 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_hh_nkvzrckj32q41pptw9t27cvc0000gn_T_main_d9ff54_mi_0, age);
}
然后搜索前一个函数的函数名 __main_block_impl_0
,发现它位于下边这个结构体里边:
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 flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock; // 指明该 block 的类型(此处是栈上的 block)。
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
而这个结构体就是 block 经编译后得到的结构,很明显 __main_block_impl_0() 是它的构造函数,我们发现,这个构造函数里边都是在给 block 的前 3 个元素 (2 个结构体和一个 int age) 赋值。
第一个元素 impl ,它的组成是这样的:
struct __block_impl {
void *isa; // 用于说明 block 的类型
int Flags; // 标识位
int Reserved; // 保留字段
void *FuncPtr; // 指针
};
FuncPtr
是一个指针,根据名字推断应该是一个函数指针,结合main()
函数中执行构造函数创建 block 的过程可以看出,FuncPtr
指向的是 block 的{ }
里边的代码构成的函数。isa
指明了block 的类型,构造函数中给它赋的值是&_NSConcreteStackBlock
,说明他是栈上的 block。关于 block 的类型,下一小节就会讲到。
第二个元素 Desc
是 __main_block_desc_0
类型的结构体,如下所示:
static struct __main_block_desc_0 {
size_t reserved; // 保留字段
size_t Block_size; // block 的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
其中 Block_size
从名字推断应该是结构体的大小,紧随其后定义了一个 __main_block_desc_0
类型的变量 __main_block_desc_0_DATA,它的第二个元素值就是当前 block 的大小 sizeof(struct __main_block_impl_0)
,从 main()
函数中执行 block 构造函数的语句可以看出,__main_block_desc_0_DATA 最终赋值给了 block 中的 Desc
,进一步验证了 Block_size 中存放的是 block 的大小。
第三个元素 int age
是 block 捕获的一个 auto 变量,关于捕获变量的机制,后面会详细讨论。
最后回到 main()
函数的最后一行代码:
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
// 简化后:
blk->FuncPtr(blk);
很明显这是执行 blk 里边的指针 FuncPtr 指向的函数,而且将 blk 自己传了进去,这样,就可以在函数内部访问到 block 捕获的变量,如前文提到的 int age。
至此,本文开头的 block 的底层结构基本介绍完了,看起来比较零散,这里绘制了一张总图做个简单小结:
2.2 Block 的类型
在此,简单说明一下 block 的类型,block 的 3 种类型及其内存分布如下:
那么这 3 种类型的 Block 有什么区别呢,为了搞清楚这个问题,我们需要先回顾一下 4 种常见的变量类型及其代码示例:
- 自动变量(auto 变量)
- 静态局部变量
- 全局变量
- 静态全局变量
// *** 4 中变量的代码示例:
// 全局变量
int global_var = 10;
// 静态全局变量
static int static_global_var = 20;
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 自动变量(局部变量)
int local_var = 30; // <==> auto int local_var = 30;
// 静态局部变量
static int local_static_var = 40;
MyBlock blk = ^{
NSLog(@"\n global_var: %d\n static_global_var: %d\n local_var: %d\n local_static_var: %d\n", global_var, static_global_var, local_var, local_static_var);
};
blk();
}
return 0;
}
关于各种 Block 的区别,可以简单汇总成下边的图表:
也就是说:
- 如果 Block 里边访问了 auto 变量,那么他就是栈上的 Block;
- 如果没有访问 auto 变量,就是全局的 Block;
- 如果对栈上的 Block 执行了 copy 操作,就变成了堆上的 Block。
2.3 Block 的 copy
上文提到了对栈上 Block 的 copy 操作,那么为什么需要 copy 呢?原因是:设置在栈上的 Block 如果其所属的作用域结束,该 Block 就会被废弃,为了延长它的生命周期,就需要将其复制到堆上。
既然栈上的 block 经 copy 后会从栈上复制到堆上,那么另外两种 Block 执行 copy 操作又会发生什么呢? 每一种 Block 被 copy 后的结果如下:
ARC 环境下,编译器会根据情况自动将栈上的 block 复制到堆上,比如满足一下条件之一时:
- block 作为函数返回值时;
- 将 block 赋值给 __strong 指针时;
- block 作为 Cocoa API 中方法名含有 usingBlock 的方法参数时;
- block 作为 GCD API 的方法参数时。
MRC 环境下,需要手动调用 block 的 copy 操作,才能将栈上的 block 复制到堆上。
3.变量捕获
为了保证 Block 内部能够正常访问外部的变量,block有个变量捕获机制,我们以前边介绍常见变量类型的代码为例,看看 Block 是怎么捕获变量的。
执行 clang -rewrite-objc main.m
之后,转换的 block 的源码如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 捕获的变量
int local_var;
int *local_static_var;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _local_var, int *_local_static_var, int flags=0) : local_var(_local_var), local_static_var(_local_static_var) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
从上边的源码可以看出来,对于这 4 种不同的变量,实际只捕获了自动变量 local_var
和静态局部标量 local_static_var
,而且自动变量是捕获了值,静态局部变量捕获的是变量地址。至于为什么这么设计,推测可能的原因如下:
3.1 捕获自动变量
实际开发中,block 捕获到的变量基本都是自动变量(局部变量),理由是:对于全局变量,任何地方都可以访问它,不安全;对于静态局部变量,它会一直存在于内存中,对内存是一种浪费。
对于基本数据类型的自动变量,前边已经讲过了,就是简单的值捕获,接下来我们重点讨论一下对象类型 auto 变量的捕获。
下边是 block 访问外部对象类型 auto 变量的简单实例。
typedef void (^MyBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
MyBlock blk = ^{
NSLog(@"%@", obj);
};
blk();
}
return 0;
}
执行 clang -rewrite-objc main.m
后,生成的源码中有这 2 点不同:
- 捕获了 NSObject *obj;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSObject *obj;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *_obj, int flags=0) : obj(_obj) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
- __main_block_desc_0 中增加了两个指针
copy
和dispose
,整个文件里新增了 2 个函数__main_block_copy_0()
和__main_block_dispose_0()
,结合上下问可以知道,这两个函数地址最终传给了 block 里 Desc 中的copy
和dispose
这 2 个指针。
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->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 从栈上拷贝到堆上
会调用 block 内部的copy()
函数,此函数内部会调用_Block_object_assign()
函数,它会根据 auto 变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成 强引用 或 弱引用。如果 block 从堆上移除
会调用 block 内部的dispose()
函数,此函数内部会调用_Block_object_dispose()
函数,它会自动释放引用的 auto 变量(即 release)。
另外,如果 block 一直是在栈上,将不会对 auto 变量产生强引用。
3.2 捕获 __block 变量
前边我们只是在 block 内部使用
变量,事实上,如果直接修改
变量的话,比如下边这个例子,就会报错:此变量不可赋值 (错误信息见注释)。
typedef void (^MyBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
MyBlock blk = ^{
age = 20; // Error: Variable is not assignable (missing __block type specifier)
NSLog(@"%d", age);
};
blk();
}
return 0;
}
按照错误信息的提示,如果给 int age = 10;
前边加上 __block
,就可以解决 block 内部无法修改 auto 变量的问题,实际操作后,发现果然可以正常输出 age 的新值 20。
3.2.1 __block 变量能够被 block 修改的原因
现在来看看 __block 修饰符到底做了什么,先将上边的代码转成 C++ 源码,下边截取了其中部分关键代码:
// 新出现的结构体
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // bound by ref
(age->__forwarding->age) = 20;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_hh_nkvzrckj32q41pptw9t27cvc0000gn_T_main_1dfa13_mi_0, (age->__forwarding->age));
}
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};
MyBlock blk = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}
return 0;
}
先看 main()
函数,__block int age
变成了 __Block_byref_age_0
类型的 age
,也就是说编译器将 __block 修饰的变量包装成了一个新的结构:
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
__Block_byref_age_0
这个结构体里边有一个 int age ,用于存储 age 的值 (10)。还有一个重要成员 __Block_byref_age_0 *__forwarding;
,结合 block 的构造函数,我们知道 __forwarding 指针实际指向了它所在的结构体。
之所以这么做是为了当 block 被拷贝到堆上以后,无论访问栈上的 block 还是 堆上的 block,最终都是访问的堆上的同一个 block(拷贝后,堆上的 __forwarding 指向自己所在的 __block 变量,栈上的 __forwarding 指向堆上的 __block 变量),如下图所示。
接下来,看看 block 的结构 __main_block_impl_0
,里边多了一个变量 __Block_byref_age_0 *age;
,即 block 捕获了这个新的结构体 __Block_byref_age_0
的地址,所以 block 里边就可以通过地址访问这个结构体,进而修改里边 int age 的值。
__block 修饰的对象类型的 auto 变量与此类似,差别仅在于新生成的结构体:
从上图可知,__block 修饰的对象类型转换后的结构体里边多了两个函数指针,他们分别指向下面 2 个函数,负责内存管理的相关操作,下边就会讲到。
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);
}
3.2.2 被 __block 修饰的对象类型的内存管理
对于 被 __block 修饰的对象类型,内存管理分以下 3 种情况:
1.当 __block 变量 在栈上时,不会对指向的对象产生强引用。
-
2.当 __block 变量 被 copy 到堆时,分两种情况:
- ARC 环境下,会调用
__block 变量内部
的 copy 函数,它会调用_Block_object_assign()
函数,此函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用。
- ARC 环境下,会调用
- MRC 环境下,不会形成强引用(retain)。
typedef void (^MyBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block NSObject *obj = [[NSObject alloc] init];
MyBlock block = [^{
NSLog(@"%p", obj);
} copy];
block();
[obj release];
}
return 0;
}
如上所示,在 MRC 环境下,对象前加了 __block,不会对 block 形成强引用, 即当执行完 [obj release];
之后,person 就被释放了。
- 3.如果 __block 变量从堆上移除,会调用
__block 变量内部
的dispose()
函数,它会调用_Block_object_dispose()
函数,此函数会自动释放指向的对象(release)
3.2.3 对象类型的auto变量 和 __block 变量
当block在栈上时,对它们都不会产生强引用
-
当 block 拷贝到堆上时,都会通过 copy 函数来处理它们
对于 __block变量(假设变量名叫做a),最终会执行
_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
-
对于对象类型的 auto 变量(假设变量名叫做p),最终会执行
_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
以上两者最终调用的方法是相同的,只不过最后一个参数有差别,前者是 8(表示引用类型),后者是 3 (表示对象),下面对
_Block_object_dispose()
函数的调用与之类似。
-
当 block 从堆上移除时,都会通过 dispose 函数来释放它们
对于 __block变量(假设变量名叫做a),最终会执行
_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
对于对象类型的auto变量(假设变量名叫做p),最终会在执行
_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
4.循环引用
关于使用 block 可能遇到的循环引用问题,我们分 ARC 和 MRC 两种情况进行讨论。
ARC 环境
在 ARC 环境下,目前大概有 3 种常见的解决循环引用的方式:
- __weak
此方式是最常见也是推荐使用的一种方式,基本原理是,self 对 block 的引用维持强引用,不过将 block 对 self 的引用改成了弱引用。
__weak typeof(self) weakSelf = self;
self.block = ^{
printf("%p", weakSelf);
};
- __unsafe_unreturned
这种方式与上边的方式类似,不过当 weakSelf 指向的对象销毁后,指针已然指向那块已经被回收的内存,可能发生野指针错误,所以是不安全的。
__unsafe_unretained typeof(self) weakSelf = self;
self.block = ^{
printf("%p", weakSelf);
};
- __block
__block typeof(self) weakSelf = self;
self.block = ^{
printf("%p", weakSelf);
weakSelf = nil;
};
self.block();
我们知道,当在变量前边加了 __block 之后就多了一个 __blcok 变量,于是里边的引用关系就变成了:
为了打破这个循环引用的关系,需要在 block 里边将对象置为 nil,而且必须执行 block 才能断开 __block 变量对对象的强引用。
MRC 环境
MRC 环境下解决循环引用的方式与 ARC 环境类似,只是由于 MRC 环境下不可以使用 weak,所以只有 __unsafe_unreturned
和 __block
2 种解决方式。对于 __block
的方式,在MRC中,__block 变量
不会对 weakSelf 产生强引用,也就不需要将其置为 nil 并执行 block 了。
__block typeof(self) weakSelf = self;
self.block = ^{
printf("%p", weakSelf);
};
5.小结
以上就是对 Block 底层实现的一个简单讨论,受自己的知识积累所限,难免有理解不到位的地方,后期会及时修正。