初探block的实现
Clang提供了中间代码展示的选项供我们进一步了解block的原理。
以一段block0.c
文件中的很简单的代码为例:
#include <stdio.h>
int main()
{
return 0;
}
复制代码
使用clang的-rewrite-objc选项生成中间代码:
clang -rewrite-objc block0.c
# 或者使用命令
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc block0.c -o block0.cpp
# 或者使用命令
clang -rewrite-objc -fobjc-arc -stdlib=libc++ -mmacosx-version-min=10.7 -fobjc-runtime=macosx-10.7 -Wno-deprecated-declarations block0.c -o block0.cpp
复制代码
查看编译后的block0.cpp
block的实现如下:
#ifndef BLOCK_IMPL
#define BLOCK_IMPL
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
// Runtime copy/destroy helper functions (from Block_private.h)
#ifdef __OBJC_EXPORT_BLOCKS
extern "C" __declspec(dllexport) void _Block_object_assign(void *, const void *, const int);
extern "C" __declspec(dllexport) void _Block_object_dispose(const void *, const int);
extern "C" __declspec(dllexport) void *_NSConcreteGlobalBlock[32];
extern "C" __declspec(dllexport) void *_NSConcreteStackBlock[32];
#else
__OBJC_RW_DLLIMPORT void _Block_object_assign(void *, const void *, const int);
__OBJC_RW_DLLIMPORT void _Block_object_dispose(const void *, const int);
__OBJC_RW_DLLIMPORT void *_NSConcreteGlobalBlock[32];
__OBJC_RW_DLLIMPORT void *_NSConcreteStackBlock[32];
#endif
#endif
复制代码
从命名可以看出这是block的实现,并且得知block在Clang编译器前端得到实现,可以生成C中间代码。很多语言都可以只实现编译器前端,生成C中间代码,然后利用现有的很多C编译器后端。
从结构体的4个成员可以看出:
- Flags可以先略过
- Reserved可以先略过
- isa指针表明了block可以是一个NSObject
- FuncPtr指针显然是block对应的函数指针。
由此,揭开了block的神秘面纱。
不过,block相关的变量放哪里呢?上面提到block可以capture词法范围内(或者说是外层上下文、作用域)的状态,即便是出了该范围,仍然可以修改这些状态。这是如何做到的呢?
一个简单的block实现
先看一个只输出一句话的block是怎么样的。
#include <stdio.h>
int main()
{
void (^blk)(void) = ^{printf("hello block!\n");};
return 0;
}
复制代码
使用clang -rewrite-objc block1.c
生成中间代码,得到如下代码片段:
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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("hello block!\n");
}
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)};
int main()
{
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
return 0;
}
复制代码
首先出现的结构体就是__main_block_impl_0
,可以看出是根据所在函数(main函数)以及出现序列(第0个)进行命名的。如果是全局block,就根据变量名和出现序列进行命名。
__main_block_impl_0中包含了两个成员变量和一个构造函数,成员变量分别是__block_impl结构体和描述信息Desc,之后在构造函数中初始化block的类型信息和函数指针等信息。
接着出现的是__main_block_func_0
函数,即block对应的函数体。该函数接受一个__cself参数,即对应的block自身。
再下面是__main_block_desc_0结构体,其中比较有价值的信息是block大小。
最后就是main函数中对block的创建和调用,可以看出执行block就是调用一个以block自身作为参数的函数,这个函数对应着block的执行体。
这里,block的类型用_NSConcreteStackBlock来表示,表明这个block位于栈中。同样地,还有_NSConcreteMallocBlock和_NSConcreteGlobalBlock。
由于block也是NSObject,我们可以对其进行retain操作。不过在将block作为回调函数传递给底层框架时,底层框架需要对其copy一份。比方说,如果将回调block作为属性,不能用retain,而要用copy。我们通常会将block写在栈中,而需要回调时,往往回调block已经不在栈中了,使用copy属性可以将block放到堆中。或者使用Block_copy()和Block_release()。
捕获局部变量的block
再看一个访问局部变量的block是怎样的。
#include <stdio.h>
int main()
{
int i = 1024;
void (^blk)(void) = ^{printf("hello block, index = %d!\n",i);};
return 0;
}
复制代码
生成中间代码:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int i;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _i, int flags=0) : i(_i) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
int i = __cself->i; // bound by copy
printf("hello block, index = %d!\n",i);
}
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)};
int main()
{
int i = 1024;
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, i));
return 0;
}
复制代码
可以看出这次的block结构体__main_block_impl_0多了个成员变量i,用来存储使用到的局部变量i(值为1024);并且此时可以看到__cself参数的作用,类似C++中的this和Objective-C的self。
如果我们尝试修改局部变量i,则会得到如下错误:
$ clang -rewrite-objc block1.c
/var/folders/l3/6z9vzk6j31dgt27r3n4j8r780000gn/T/block1-fd3e93.i:442:11: error: variable is not assignable (missing __block type specifier)
i = 0;
~ ^
1 error generated.
复制代码
错误信息很详细,既告诉我们变量不可赋值,也提醒我们要使用__block类型标识符。
为什么不能给变量i赋值呢?
因为main函数中的局部变量i和函数__main_block_func_0不在同一个作用域中,调用过程中只是进行了值传递。当然,在上面代码中,我们可以通过指针来实现局部变量的修改。不过这是由于在调用__main_block_func_0时,main函数栈还没展开完成,变量i还在栈中。但是在很多情况下,block是作为参数传递以供后续回调执行的。通常在这些情况下,block被执行时,定义时所在的函数栈已经被展开,局部变量已经不在栈中了(block此时在哪里?),再用指针访问就非法了。
所以,对于auto类型的局部变量,不允许block进行修改是合理的。
block修改本地变量
那么,__block类型变量是如何支持修改的呢?
#include <stdio.h>
int main()
{
__block int i = 1024;
void (^blk)(void) = ^{
i = 1025;
printf("hello block, index = %d!\n",i);
};
blk();
return 0;
}
复制代码
我们为int类型变量加上__block指示符,使得变量i可以在block函数体中被修改。
此时再看中间代码,会多出很多信息。首先是__block变量对应的结构体:
struct __Block_byref_i_0 {
void *__isa;
__Block_byref_i_0 *__forwarding;
int __flags;
int __size;
int i;
};
复制代码
由第一个成员__isa指针也可以知道__Block_byref_i_0也可以是NSObject。
第二个成员__forwarding指向自己,为什么要指向自己?指向自己是没有意义的,只能说有时候需要指向另一个__Block_byref_i_0结构。
最后一个成员是目标存储变量i。
此时,__main_block_impl_0结构如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_i_0 *i; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
复制代码
__main_block_impl_0的成员变量i变成了__Block_byref_i_0 *类型。
对应的函数__main_block_func_0如下:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_i_0 *i = __cself->i; // bound by ref
(i->__forwarding->i) = 1025;
printf("hello block, index = %d!\n",(i->__forwarding->i));
}
复制代码
亮点是__Block_byref_i_0指针类型变量i,通过其成员变量__forwarding指针来操作另一个成员变量。 🙂
而main函数如下:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);
}
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};
int main()
{
__attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 1024};
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
复制代码
通过这样看起来有点复杂的改变,我们可以修改变量i的值。但是问题同样存在:__Block_byref_i_0类型变量i仍然处于栈上,当block被回调执行时,变量i所在的栈已经被展开,怎么办?
在这种关键时刻,__main_block_desc_0站出来了,此时,__main_block_desc_0多了两个成员函数:copy和dispose,分别指向__main_block_copy_0和__main_block_dispose_0。
当block从栈上被copy到堆上时,会调用__main_block_copy_0将__block类型的成员变量i从栈上复制到堆上;而当block被释放时,相应地会调用__main_block_dispose_0来释放__block类型的成员变量i。
一会在栈上,一会在堆上,那如果栈上和堆上同时对该变量进行操作,怎么办?
这时候,__forwarding的作用就体现出来了:当一个__block变量从栈上被复制到堆上时,栈上的那个__Block_byref_i_0结构体中的__forwarding指针也会指向堆上的结构。
日常开发中的block实现探究
参考如下MCBlock.m
实现代码:
#import "MCBlock.h"
@implementation MCBlock
// 全局变量
int global_var = 4;
- (void)method
{
int var1 = 1;
__unsafe_unretained id unsafe_obj = nil;
__strong id strong_obj = nil;
static int static_var = 3;
void (^my_Block)(void) = ^{
NSLog(@"局部变量<基本数据类型> var %d",var1);
NSLog(@"局部变量<__unsafe_unretained对象类型> var %@",unsafe_obj);
NSLog(@"局部变量<strong对象类型> var %@",strong_obj);
NSLog(@"静态变量 %d",static_var);
};
my_Block();
}
@end
复制代码
使用clang -rewrite-objc MCBLock.m
生成中间代码,得到的block部分的如下的实现代码片段:
int global_var = 4;
// block实现的结构体
struct __MCBlock__method_block_impl_0 {
struct __block_impl impl;
struct __MCBlock__method_block_desc_0* Desc;
int var1;
id unsafe_obj;
id strong_obj;
int *static_var;
__MCBlock__method_block_impl_0(void *fp, struct __MCBlock__method_block_desc_0 *desc, int _var1, id _unsafe_obj, id _strong_obj, int *_static_var, int flags=0) : var1(_var1), unsafe_obj(_unsafe_obj), strong_obj(_strong_obj), static_var(_static_var) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __MCBlock__method_block_func_0(struct __MCBlock__method_block_impl_0 *__cself) {
int var1 = __cself->var1; // bound by copy
id unsafe_obj = __cself->unsafe_obj; // bound by copy
id strong_obj = __cself->strong_obj; // bound by copy
int *static_var = __cself->static_var; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_l3_6z9vzk6j31dgt27r3n4j8r780000gn_T_MCBlock_27310d_mi_0,var1);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_l3_6z9vzk6j31dgt27r3n4j8r780000gn_T_MCBlock_27310d_mi_1,unsafe_obj);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_l3_6z9vzk6j31dgt27r3n4j8r780000gn_T_MCBlock_27310d_mi_2,strong_obj);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_l3_6z9vzk6j31dgt27r3n4j8r780000gn_T_MCBlock_27310d_mi_3,(*static_var));
}
static void __MCBlock__method_block_copy_0(struct __MCBlock__method_block_impl_0*dst, struct __MCBlock__method_block_impl_0*src) {_Block_object_assign((void*)&dst->unsafe_obj, (void*)src->unsafe_obj, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_assign((void*)&dst->strong_obj, (void*)src->strong_obj, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __MCBlock__method_block_dispose_0(struct __MCBlock__method_block_impl_0*src) {_Block_object_dispose((void*)src->unsafe_obj, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_dispose((void*)src->strong_obj, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static struct __MCBlock__method_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __MCBlock__method_block_impl_0*, struct __MCBlock__method_block_impl_0*);
void (*dispose)(struct __MCBlock__method_block_impl_0*);
} __MCBlock__method_block_desc_0_DATA = { 0, sizeof(struct __MCBlock__method_block_impl_0), __MCBlock__method_block_copy_0, __MCBlock__method_block_dispose_0};
static void _I_MCBlock_method(MCBlock * self, SEL _cmd) {
int var1 = 1;
__attribute__((objc_ownership(none))) id unsafe_obj = __null;
__attribute__((objc_ownership(strong))) id strong_obj = __null;
static int static_var = 3;
void (*my_Block)(void) = ((void (*)())&__MCBlock__method_block_impl_0((void *)__MCBlock__method_block_func_0, &__MCBlock__method_block_desc_0_DATA, var1, unsafe_obj, strong_obj, &static_var, 570425344));
((void (*)(__block_impl *))((__block_impl *)my_Block)->FuncPtr)((__block_impl *)my_Block);
}
复制代码
block在内存中的位置
在ARC环境下,block只会保存在两个地址:全局区和堆区
在非ARC环境下,才会保存在全局区(全局block,不捕获变量的局部block),栈区(捕获变量的局部block)和堆区三种地方。
所以为什么对象的property声明的时候,需要保证是使用copy,而不是strong,就是这个原因,保证在ARC和非ARC下进行统一。
参考
QA
基本类型的变量和对象类型的变量被__block修饰与否各有什么区别?
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
复制代码
小结
当我们创建一个block,并调用之,编译器为我们做的事情如下:
- 创建block所有的Struct代码:一个构造函数,一个真正的执行代码函数,一个描述信息(假如使用了局部变量,则会有每个局部变量的定义)。
- 将我们的创建代码转码为block_impl的构造语句。
- 将我们的执行语句转码为对block的执行函数的调用。