上一篇关于 Block 基础知识的文章里,提到 Block 是对标准 C 的拓展,它的底层还是依赖标准 C/C++ 实现的。这篇文章就来揭秘一下 Block 的底层实现。
利用 clang 转译 Objc 源码
利用 clang 的 -rewrite-objc
参数可以将 Objc 源码转译为标准 C 代码(输出文件格式为 .cpp
)。虽说输出是 C++ 文件,但其实内部主要还是用标准 C 写的,只不过某些用到了 struct 构造器的地方是 C++ 的特性。
在终端直接使用 clang -rewrite-objc file.m
指令转译 Objc 代码会出现找不到系统库头文件的问题。这时候需要在指令里加上 -isysroot xcrun --show-sdk-path
指定 SDK 的路径:
shell
clang -isysroot `xcrun --show-sdk-path` -rewrite-objc file.m
💡 如果想要在 ARC 模式下转译的话,需要加上 -fobjc-arc
参数:
shell
clang -isysroot `xcrun --show-sdk-path` -rewrite-objc -fobjc-arc block_auto_copy.m
C++ 中的 this 和 Objective-C 中的 self
C++ 源码也能借助 clang 转成标准 C 实现,我们借此先了解一下 this
和 self
。
C++ 中的 this
在 C++ 中定义一个实例方法,并在 main
函数中调用:
cpp
#include <stdio.h>
class MyClass {
public:
void method(int param);
};
void MyClass::method(int param) {
printf("%p %d\n", this, param);
}
int main() {
MyClass obj;
obj.method(10);
}
将上面代码转译为标准 C 实现:
c
#include <stdio.h>
struct MyClass {
void (*method)(struct MyClass* self, int param);
};
void method(struct MyClass* self, int param) {
printf("%p %d\n", self, param);
}
int main() {
struct MyClass obj;
obj.method = method;
obj.method(&obj, 10);
}
可以看到 C++ 的类在标准 C 里面是使用 struct 实现的。实例方法 method
转为标准 C 实现后,变成了一个单独的函数,并且参数里多了一个 struct Myclass* self
,即实例本身(实际为 struct)。Myclass
结构体里面存储了 method
的指针,调用实例方法时通过这个指针找到对应的函数。
实例在调用自己的实例方法时,也是将自己的地址作为参数传递了进去:cls.method(&obj, 10);
。这就是 C++ 中的 this
。
Objective-C 中的 self
同样使用 clang 将下面这段代码转译为标准 C 实现:
objectivec
#include <Foundation/Foundation.h>
@interface MyClass : NSObject
@end
@implementation MyClass
- (void)method:(int)param {
NSLog(@"%p %d\n", self, param);
}
@end
int main(int argc, const char * argv[]) {
MyClass *obj = [[MyClass alloc] init];
[obj method:10];
return 0;
}
标准 C 实现:
c
#ifndef _REWRITER_typedef_MyClass
#define _REWRITER_typedef_MyClass
typedef struct objc_object MyClass;
typedef struct {} _objc_exc_MyClass;
#endif
struct MyClass_IMPL {
struct NSObject_IMPL NSObject_IVARS;
};
/* @end */
// @implementation MyClass
static void _I_MyClass_method_(MyClass * self, SEL _cmd, int param) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kk_801wlmp11577d334vqtg_vgr0000gn_T_main_4bc26d_mii_0, self, param);
}
// @end
int main(int argc, const char * argv[]) {
MyClass *obj = ((MyClass *(*)(id, SEL))(void *)objc_msgSend)((id)((MyClass *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MyClass"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL, int))(void *)objc_msgSend)((id)obj, sel_registerName("method:"), 10);
return 0;
}
和 C++ 一样,self
也是被作为了一个参数传递给了 _I_MyClass_method_
函数。而 _I_MyClass_method_
函数对应着源码 method
函数,且变成了一个静态函数。静态函数的好处之一就是其他文件中可以定义相同名字的函数,不会发生冲突。
在调用 method
函数时,标准 C 实现里使用了 objc_msgSend
函数。这也是我们常说的,Objc 的方法调用其实就是向对象发送消息。objc_msgSend
函数会通过对象名和方法名检索 _I_MyClass_method_
的函数指针,通过函数指针调用函数,并将实例本身 obj
作为第一个参数传递进去。
Blocks 的底层实现
简易 Block 的标准 C/C++ 实现
利用 clang 将下面代码转译为标准 C/C++ 代码:
objectivec
int main(int argc, const char * argv[]) {
void (^blk)(void) = ^{
printf("Block\n");
};
blk();
return 0;
}
转译结果:
objc
#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
#define __block
#define __weak
// 省略了一些无关的代码
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("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(int argc, const char * argv[]) {
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
从标准 C/C++ 代码里可以看出来,Block 在底层是利用一个主逻辑函数和一个负责存储数据的结构体实现的 。函数 __main_block_func_0
负责处理 Block 的主要计算逻辑,而存储数据的结构体 __main_block_impl_0
负责存储前者的指针(函数指针),以及需要给函数传递的参数等。在后面的章节里面,我们还会发现,Block 外部被捕获的变量也会存储在 __main_block_impl_0
里。
函数 __main_block_func_0
函数接受一个 _cself
参数,这个参数就类似于 C++ 中的 this,以及 Objc 中的 self。只不过 _cself
代表着 Block 自己,相当于把 Block 当成一个对象了。
__main_block_func_0
以及 __main_block_impl_0
这俩名字是根据原始 Objc 代码中 Block 声明所在的函数的名字,以及是函数中第几个 Block 来定下的。
这就是一个简单的 Block 的底层实现,接下来我们会更深入地分析底层实现的源码。
__cself 结构体的结构
在上面转译之后的 C 代码中,__main_block_func_0
函数接受的 _cself
参数是 __main_block_impl_0
类型的。其结构体定义如下:
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;
}
};
这是一个比较简单的结构体,还没有捕获外界的变量。里面包含了三个要素:
-
__block_impl
类型的impl
。是一个结构体,存储 Block 的基本实现信息,比如对应的函数指针。 -
__main_block_desc_0
类型的Desc
。也是一个结构体,存储 Block 的 size 等信息。定义如下,里面直接定义了一个__main_block_desc_0
类型的静态全局变量 __main_block_desc_0_DATA,方便创建__main_block_impl_0
时直接使用。cppstatic 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_impl_0
结构体的构造器。
其中 __block_impl
结构体是 Blocks 通用的,并不是专门为某个 Block 定义的。定义如下:
cpp
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
里面存储了 Block 对应的实现函数的指针,标志,isa 等信息,可以称作 Block 的「基类」。
简化一下__main_block_impl_0
结构体,去掉构造函数:
cpp
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
};
这个结构体整体可以作为 Block 的身份信息,也可以说是 Block 的类似于 class 的声明,这就是 __cself
的意义。
__main_block_impl_0
内部还定义了一个构造器:
c
__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;
}
这个构造器接收上面提到的几个要素作为参数,来创建 __main_block_impl_0
的实例。
调用这个构造器的代码:
c
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
第一行代码调用了 __main_block_impl_0
的构造函数,将生成的 __main_block_impl_0
实例的地址赋给了变量 blk。也就是说 blk 目前是 __main_block_impl_0 类型的指针,并且其指向的实例是一个在栈上创建的局部变量(MRC 环境下)。
__main_block_func_0 的调用逻辑
未转译时的 Objc 源码在执行 Block 时的代码是 blk();
,这行代码对应于转译后的代码:
c
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
因为 blk 的类型是 __main_block_impl_0
,其结构体中第一个元素是 __block_impl
类型的 impl。从内存结构上来说,可以通过成员 impl 的偏移量(是 0)来直接获取成员的地址并访问成员。因此 (__block_impl *)blk
这块代码直接将 blk 转成了 __block_impl
类型。
关于 struct 的内存结构,可以参考我之前的 post:
去掉各种类型转换,可以简化为:
c
(*blk->impl.FuncPtr)(blk);
简化后的代码一目了然,调用了 FuncPtr 指向的函数,并将 blk 自己作为 __cself
传递了进去。
isa = &_NSConcreteStackBlock 的含义
在 __main_block_impl_0
的构造器中,有这样一句代码:
cpp
impl.isa = &_NSConcreteStackBlock;
这句代码将_NSConcreteStackBlock
的地址赋给了 isa。为了理解 isa 是什么,我们首先得知道 Objective-C 中的 class 和 object 是怎么实现的。
在 Objective-C 中我们经常将一个对象的指针赋值给一个 id
类型的变量,它就像 C 语言中的 void *
。其实 id
也是在 C 语言中声明和实现的:
cpp
// objc.h
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
id
是一个指向 objc_object 类型的指针。而 Class 的定义为:
cpp
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
struct objc_class : objc_object {
Class superclass;
}
objc_object 和 objc_class 是 Objective-C 中对象和类最基础的结构体。objc_object 指代类的实例,而 objc_class 指代实例的类型。其实不难发现,objc_class 其实构成了类似于链表的结构,内部装了同一类型的另一个变量。逐个遍历这个链表,就能一层一层拿到父级、父级的父级等的类型信息。
💡 objc_object 和 objc_class 的源码可以在 Apple 的 objc 开源仓库里找到。
isa = &_NSConcreteStackBlock
意思是当 Block 被视为一个 object 时,我们能从 _NSConcreteStackBlock
中溯源到我们想要的关于 Block 类型的信息。
至于 _NSConcreteStackBlock
,我们后面的章节会具体讲到它以及和它相关的另外几种类型。
Block 对局部变量的捕获及修改
接下里的篇章就是重头戏了。Block 最重要的能力就是能捕获局部变量,关联上下文信息。
捕获变量的底层实现
将下面这段代码转换成标准 C/C++ 实现。
objectivec
int main() {
int dmy = 256;
int val = 10;
const char *fmt = "val = %d\n";
void (^blk)(void) = ^{ printf(fmt, val); };
blk();
return 0;
}
转换结果:
objectivec
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
const char *fmt;
int val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
const char *fmt = __cself->fmt; // bound by copy
int val = __cself->val; // bound by copy
printf(fmt, val); }
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 dmy = 256;
int val = 10;
const char *fmt = "val = %d\n";
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
在 blk 中使用到的局部变量 fmt
和 val
在 __main_block_impl_0
中有了对应的成员变量,而 blk 没有使用的局部变量 dmy
则没有处理。
__main_block_impl_0
的构造函数中也多了对 fmt
和 val
的初始化操作。在对 blk 进行初始化时,将局部变量 fmt 和 val 作为参数传递给了 __main_block_impl_0
的构造函数。
objectivec
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val)
这不仅印证了上面提到的,__main_block_impl_0
是专门存储数据的,包括了上下文信息;也解释了为什么 Block 里面的 self 要用 __weak
来处理------__main_block_impl_0
里面又(隐式)强持有了 self。
为什么 Block 不能捕获 C 数组
源码 ^{ printf(fmt, val); }
对应的标准 C/C++ 实现代码对应着 __main_block_func_0
函数。
c
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
const char *fmt = __cself->fmt; // bound by copy
int val = __cself->val; // bound by copy
printf(fmt, val);
}
变量 fmt 和 val 现在是从 __cself 中取出的,也就是从 __main_block_impl_0
里面取出来的。
前面章节中说过,Blocks 是无法捕获 C 数组的。至于为什么不能这么干,我们可以先写个简短的代码实验。
c
char a[10] = "hello";
char b[10] = a;
上面这段代码编译时会报错:Array initializer must be an initializer list or string literal.
意思是必须用字面意义上的数组或者字符串初始化数组,不能用另外一个数组直接初始化。这就是为啥 Block 无法捕获 C 数组的原因。如果 Block 捕获了 C 数组,那么之后在执行 Block 内部代码时,肯定会有类似 int val = __cself->val;
的操作;如果把这个操作放在 C 数组上,那就是用一个 现有的 C 数组来初始化一个局部变量,会报编译错误。
修改被捕获的局部变量的值
一般情况下,局部变量(非全局/静态)被捕获后,在 struct 内部生成的与之对应的变量(__main_block_impl_0
里面的 val
,下称"镜像变量")是不允许修改值的。因为编译器是把被捕获变量的值赋给了镜像变量,并不是让镜像变量指向被捕获变量,他们是两个独立的变量。如果镜像变量能修改值的话,那其意义就不存在了。
尽管如此,我们还是有一些场景需要修改被捕获变量的值,比如在 Block 中修改计次数。
使用静态变量或者全局变量
前面说到被捕获的变量和镜像变量是相互独立的,但也有特例,比如静态变量或全局变量。在 C 语言中,我们经常使用以下几种变量做持久存储或共享信息:
- 静态变量
- 静态全局变量
- 全局变量
全局变量和静态全局变量能够在同一文件中任意地方访问。因此在将源码转成标准 C/C++ 实现后,Block 的逻辑函数里也能对全局变量和静态全局变量直接访问,不需要生成镜像变量来存储全局变量/静态全局变量的值。
用下面这段代码做个实验:
objectivec
int global_val = 1;
static int static_global_val = 2;
int main() {
static int static_val = 3;
void (^blk)(void) = ^ {
global_val *= 1;
static_global_val *= 2;
static_val *= 3;
};
blk();
return 0;
}
转换成标准 C/C++ 实现:
objectivec
int global_val = 1;
static int static_global_val = 2;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_val = __cself->static_val; // bound by copy
global_val *= 1;
static_global_val *= 2;
(*static_val) *= 3;
}
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() {
static int static_val = 3;
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
可以发现,全局变量 global_val 和静态全局变量 static_global_val 在 block 的实现函数 __main_block_impl_0
里并没有生成对应的镜像变量,在逻辑函数 __main_block_func_0
里也是直接访问的这两个变量。
但静态变量有些不同,因为静态变量尽管生命周期比普通的局部变量长,但是其作用域还是被限制在了函数内部,无法在 main 函数外访问,也无法在 __main_block_func_0
里访问。
静态变量毕竟是和普通变量有很大差别的,如果按照普通变量的方式给静态变量生成一个镜像变量,那绝对是不能的。标准 C/C++ 实现中解决这一问题的方式是使用指针,让指针指向静态变量。在 __main_block_impl_0
的构造过程中传入了 static_val 的地址:
objectivec
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val));
使用 __block 关键字
在 C 语言中,有下面几个存储类型关键字,表明变量存储的位置:
- typedef 为现有类型添加一个同义字
- extern 声明一个已经在别处定义了的变量
- static 静态变量
- auto 变量具有自动存储时期,一般是存储在栈区的局部变量
- register 将一个变量归入寄存器存储类,具备更快的读写速度
__block 关键字和上面这几个关键字类似,也是说明了变量的存储类型,下面我们来具体看一下。
老规矩,还是看下 __block 的标准 C/C++ 实现:
objectivec
int main() {
__block int val = 10;
void (^blk)(void) = ^ {
val = 1;
};
}
转译后:
objectivec
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_val_0 *val; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__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_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 1;
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 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_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
}
原始代码里面简单的 __block int val = 10;
,在标准实现里面变成了:
objectivec
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
val
在标准实现里面变成了 __Block_byref_val_0
类型的结构体,不再是一个单一的变量。在这个结构体里面,又有一个 int
类型的成员变量 val
,存储着原始的 val
,它是可以更改值的。
在 Block 里面修改局部变量 val
的值,对应着修改 __Block_byref_val_0
里面的 val
:
objectivec
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 1;
}
每个被 __block 修饰的变量都会在标准 C/C++ 实现里生成一个类似 __Block_byref_val_0
的结构体。如果有多个 Block 内部都要修改同一个被 __block 修饰的变量的话,这些 Block 被初始化时会接受这个变量对应的 __Block_byref_val_0
类型的结构体地址:
objectivec
__block int val = 10;
void (^blk)(void) = ^ {
val = 1;
};
void (^blk1)(void) = ^ {
val = 2;
};
转译后:
objectivec
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
void (*blk1)(void) = ((void (*)())&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA, (__Block_byref_val_0 *)&val, 570425344));
把 __Block_byref_val_0
这种类型的结构体单独拆出来,是为了方便多个 Block 捕获同一个 __block 变量。
看到这里,我们应该大致理解了 __block 关键字的原理:被 __block 标记的变量会在底层实现里变成一个结构体,这个结构体可以在 Block 之间共享,修改变量其实是修改这个结构体里面的变量。
但是我们还有个疑惑,__Block_byref_val_0
这个结构体是在栈上创建的,而 Block 是要从栈上拷贝到堆上的(生命周期比函数/方法长),__Block_byref_val_0
是怎么跟随 Block 一起被拷贝到堆上的?
Block 的内存结构
Block 的存储区域
前面的章节里面提到了 _NSConcreteStackBlock
,与之同类的有以下几个:
_NSConcreteStackBlock
_NSConcreteGlobalBlock
_NSConcreteMallocBlock
它们代表了 Block 不同的存储区域。
图源:Pro Multithreading and Memory Management for iOS and OS X with ARC, Grand Central Dispatch, and Blocks (Kazuki Sakamoto, Tomohiko Furumoto)
存储在 Data 区的 Block ------ _NSConcreteGlobalBlock
有两种 Block 会被存放在 Data 区:
-
像全局变量一样,声明在函数外面的 Block:
cppvoid (^blk)(void) = ^{printf("Global Block\n");}; int main() { ... }
-
Block 内部没有捕获局部变量,逻辑与外界独立:
cpptypedef int (^blk_t)(int); for (int rate = 0; rate < 10; ++rate) { blk_t blk = ^(int count){ return count; }; }
Data 区的内存在编译时就已确定好,在程序整个运行期间都存在。把数据存储在 Data 区能够减少数据的创建和释放,加快执行以及访问速度。
存储在堆区的 Block ------ _NSConcreteMallocBlock
在 Data 区存储的 Block 有着和程序一样长的生命周期,在栈区存储的 Block 会在超出作用域后释放,而存储在堆区的 Block 具备更灵活的生命周期。
Block 提供了从栈区复制到堆区的能力。从栈区复制到堆区后,Block 的 isa 属性值会从 &_NSConcreteMallocBlock
变成 &_NSConcreteMallocBlock
:
c
impl.isa = &_NSConcreteMallocBlock;
Block 被拷贝到堆区后,可以通过 __forwarding
属性获取到堆上的 Block。
自动/手动拷贝 Block
在 ARC 环境下,编译器会自动检测并把 Block 从栈区拷贝到堆区,甚至是在创建时就直接把 Block 拷贝到了堆区(因为隐式强持有)。
💡 前面的代码转译大部分是在 MRC 环境下进行的,因为没有加 -fobjc-arc
参数。
将下面的代码片段通过 clang 编译执行(注意不是转译,是直接编译执行):
objectivec
#include <Foundation/Foundation.h>
typedef int (^blk_t)(int);
blk_t func(int rate) {
blk_t blk = ^(int count){
return rate * count;
};
NSLog(@"blk: %@", blk);
blk_t blk1 = ^(int count){
return rate * count;
};
NSLog(@"blk1: %@", blk1);
return blk1;
}
int main(int argc, const char * argv[]) {
blk_t blk = func(10);
NSLog(@"blk main: %@", blk);
return 0;
}
💡 使用 clang -isysroot
xcrun --show-sdk-path -fno-objc-arc -framework Foundation block_copy.m -o block_copy
编译单个 .m 文件。-fno-objc-arc
和 fobjc-arc
分别控制是否在 ARC 环境下执行。
-
ARC 执行结果:
objectivec2023-10-08 19:15:32.227 block_copy[18722:5025896] blk: <__NSMallocBlock__: 0x6000026f8660> 2023-10-08 19:15:32.227 block_copy[18722:5025896] blk1: <__NSMallocBlock__: 0x6000026fc000> 2023-10-08 19:15:32.228 block_copy[18722:5025896] blk main: <__NSMallocBlock__: 0x6000026fc000>
-
非 ARC 执行结果:
objectivec2023-10-08 18:57:25.187 block_copy[18054:5000955] blk: <**NSStackBlock**: 0x16b75f128> 2023-10-08 18:57:25.187 block_copy[18054:5000955] blk1: <**NSStackBlock**: 0x16b75f0f8> [1] 18054 segmentation fault ./block_copy
在 ARC 环境下,因为局部变量对 Block 是强持有的,所以Blocks 被自动拷贝到了堆区。被拷贝到堆区的 Block 不会在函数执行完毕后释放,因此在 main 函数里赋值没有问题。
而非 ARC 环境下,Blocks 没有被自动拷贝到堆区,还是在栈区。而且由于栈区的 Block 在函数结束后被释放了,因此在 main 函数里接受的返回值是空的,导致程序执行出错。
在 ARC 环境下,大部分 Block 的都被编译器在堆区或者 Data 区初始化。可以说,只要有变量(不管是局部变量还是其他变量)强持有了 Block,那 Block 就不大可能是存在栈区的。而且在 ARC 环境下,变量默认是强持有的,除非用 weak
或 __weak
指定弱持有。
以下面代码为例,调用其中的 testCopy
方法。
objectivec
- (void)testCopy {
NSArray *arr = [self createBlocks];
typedef void (^blk_t)(void);
blk_t arr_blk = (blk_t)[arr objectAtIndex:0];
NSLog(@"arr_blk: %@", arr_blk);
arr_blk();
__weak blk_t weak_arr_blk = (blk_t)[arr objectAtIndex:1];
NSLog(@"weak_arr_blk: %@", weak_arr_blk);
weak_arr_blk();
int val = 0;
__weak blk_t weak_blk = ^{ NSLog(@"val: %d", val); };
NSLog(@"weak_blk: %@", weak_blk);
blk_t strong_blk = ^{ NSLog(@"val: %d", val); };
NSLog(@"strong_blk: %@", strong_blk);
}
- (id)createBlocks {
int stackValue = 1;
NSLog(@"stack value address: %p", &stackValue);
// __block int val = 10;
int val = 10;
NSLog(@"val address: %p", &val);
return [[NSArray alloc] initWithObjects:^{ NSLog(@"blk0: %d, val pointer: %p", val, &val); }, ^{ NSLog(@"blk1: %d", val); }, nil];
}
输出为:
objectivec
stack value address: 0x16fdff0dc
val address: 0x16fdff0d8
arr_blk: <__NSMallocBlock__: 0x6000012a0000>
blk0: 10, val pointer: 0x6000012a0020
weak_arr_blk: <__NSMallocBlock__: 0x6000012a0030>
blk1: 10
weak_blk: <__NSStackBlock__: 0x16fdff1a0>
strong_blk: <__NSMallocBlock__: 0x6000012a8330>
- 通过
NSArray
的initWithObjects
方法传入的 Block 都变成了 NSMallocBlock,触发了编译器的自动拷贝。这一点和原书中讲述的不一致:原书中说这种作为参数传递给函数或方法的 Block 是 NSStackBlock,且不会出发自动拷贝,会在函数结束后释放,导致后面在数组里取 Block 元素并执行的时候报错。 - Block 类型的变量强持有的 Block 是堆区 Block。由于数组也是强持有 Block,所以数组里面的 Block 元素被拷贝到了堆区
- Block 变量弱持有的 Block 是栈区 Block
另外也可以通过 copy
方法主动将栈区的 Block 拷贝到堆区:
objectivec
- (void)testStackCopytoHeap {
int val = 0;
typedef void (^blk_t)(void);
__weak blk_t weak_blk = ^{ NSLog(@"val: %d", val); };
NSLog(@"stack_blk: %@", weak_blk);
NSLog(@"heap_blk: %@", [weak_blk copy]);
}
输出:
objectivec
stack_blk: <__NSStackBlock__: 0x16fdff1d8>
heap_blk: <__NSMallocBlock__: 0x600002b243f0>
调用 copy
方法后,栈区的 Block 被拷贝到了堆区。
__block 变量的内存区域
前面的部分总结了 Block 由栈区拷贝到堆区的规则。这一小节会总结下由 __block
修饰的变量的内存拷贝原则。
先记住总的原则:当 Block 由栈区拷贝到堆区后,其捕获的 __block
变量也会由栈区拷贝到堆区,拷贝完成后 Block 依旧对 __block
变量有所属权。
拷贝流程:
-
起初假设 Block 和
__block
变量都在栈区。 -
当 Block 被拷贝到堆区后,其持有的
__block
变量也会被拷贝到堆区。 -
如果是多个 Block 都对
__block
变量拥有所属权,__block
变量仅会经历一次从栈区拷贝到堆区的操作,不会有从堆区拷贝到堆区的操作。
- Block 释放后,如果不再有 Block 持有
__block
变量,那么__block
变量将随之释放。
当 __block
变量拷贝到堆区前后,Block 内部使用这个变量时都是通过 __forwarding
来获取当前实际的变量。
__forwarding
__Block_byref_val_0
结构体里面的 __forwarding
属性是为了追踪 __block
变量的实际位置的。如果 __block
变量处于栈区,那么 __forwarding
将是一个指向栈区变量的指针;如果 __block
变量被拷贝到了堆区,那么 __forwarding
将同步变成指向堆区变量的指针。
在上面转译后的源码的 main
函数里,有这样的一段代码:
objectivec
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
这段代码在栈区创建了一个 __Block_byref_val_0 类型的 val,并将其地址作为参数传递给了 __main_block_impl_0 类型的 blk 变量。虽然说 Block 能通过 (val->__forwarding->val)
来获取到实际的 val,但是当 Block 被拷贝到堆区,函数执行完毕后,栈区的 val 应该会被释放掉,这时候再调用 (val->__forwarding->val)
是如何保证不出现崩溃问题的?
在转译代码中,有 Block 的 copy 和 dispose 函数的实现:
objectivec
static void __main_block_copy_1(struct __main_block_impl_1*dst, struct __main_block_impl_1*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_1(struct __main_block_impl_1*src) {_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
其中用到了 _Block_object_assign
和 _Block_object_dispose
函数,这两个函数接受的并不是 __main_block_impl_1
类型的参数,而是 __Block_byref_val_0
类型的参数,也就是 __block
变量的底层结构体。
clang 转译的代码中没有说 Block 以及 __block
变量具体是怎么被拷贝的,需要查看关键函数 _Block_object_assign
的实现。源码可以在 ‣ 中的 runtime.c 文件找到。
objectivec
//
// When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point
// to do the assignment.
//
void _Block_object_assign(void *destArg, const void *object, const int flags) {
const void **dest = (const void **)destArg;
switch (flags & BLOCK_ALL_COPY_DISPOSE_FLAGS) {
case BLOCK_FIELD_IS_OBJECT:
/*******
id object = ...;
[^{ object; } copy];
********/
_Block_retain_object(object);
*dest = object;
break;
case BLOCK_FIELD_IS_BLOCK:
/*******
void (^object)(void) = ...;
[^{ object; } copy];
********/
*dest = _Block_copy(object);
break;
case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
case BLOCK_FIELD_IS_BYREF:
/*******
// copy the onstack __block container to the heap
// Note this __weak is old GC-weak/MRC-unretained.
// ARC-style __weak is handled by the copy helper directly.
__block ... x;
__weak __block ... x;
[^{ x; } copy];
********/
*dest = _Block_byref_copy(object);
break;
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
// Note this is MRC unretained __block only.
// ARC retained __block is handled by the copy helper directly.
__block id object;
__block void (^object)(void);
[^{ object; } copy];
********/
*dest = object;
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_WEAK:
/*******
// copy the actual field held in the __block container
// Note this __weak is old GC-weak/MRC-unretained.
// ARC-style __weak is handled by the copy helper directly.
__weak __block id object;
__weak __block void (^object)(void);
[^{ object; } copy];
********/
*dest = object;
break;
default:
break;
}
}
函数里面根据不同的 flag 参数走到了不同的分支,转译代码中用的 flag 是 8/*BLOCK_FIELD_IS_BYREF*/
。那么接下来会调用 _Block_byref_copy
函数,传递进去的参数是拷贝前的 __Block_byref_val_0
。
_Block_byref_copy
函数源码:
objectivec
// Runtime entry points for maintaining the sharing knowledge of byref data blocks.
// A closure has been copied and its fixup routine is asking us to fix up the reference to the shared byref data
// Closures that aren't copied must still work, so everyone always accesses variables after dereferencing the forwarding ptr.
// We ask if the byref pointer that we know about has already been copied to the heap, and if so, increment and return it.
// Otherwise we need to copy it and update the stack forwarding pointer
static struct Block_byref *_Block_byref_copy(const void *arg) {
struct Block_byref *src = (struct Block_byref *)arg;
if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
// src points to stack
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
copy->isa = NULL;
// byref value 4 is logical refcount of 2: one for caller, one for stack
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
copy->forwarding = copy; // patch heap copy to point to itself
src->forwarding = copy; // patch stack to point to heap copy
copy->size = src->size;
if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
// Trust copy helper to copy everything of interest
// If more than one field shows up in a byref block this is wrong XXX
struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
copy2->byref_keep = src2->byref_keep;
copy2->byref_destroy = src2->byref_destroy;
if (BLOCK_BYREF_LAYOUT(src) == BLOCK_BYREF_LAYOUT_EXTENDED) {
struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
copy3->layout = src3->layout;
}
(*src2->byref_keep)(copy, src);
}
else {
// Bitwise copy.
// This copy includes Block_byref_3, if any.
memmove(copy+1, src+1, src->size - sizeof(*src));
}
}
// already copied to heap
else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
latching_incr_int(&src->forwarding->flags);
}
return src->forwarding;
}
因为 val
(这里指的是 __block 变量的底层结构体)在拷贝前是在栈区,所以会走到 if 分支里。如果走到 else if 里面,就说明 val
已经被拷贝到堆区了。随后调用了 malloc
函数,在堆区给新的 val
申请了内存空间。比较关键的是这两行代码:
objectivec
copy->forwarding = copy; // patch heap copy to point to itself
src->forwarding = copy; // patch stack to point to heap copy
这两行代码让堆区新拷贝的和原来栈区没释放的 val
的 __forwarding
都指向了堆区新拷贝的 val
。也就是说,拷贝完成后,通过 __forwarding
获取的 val
肯定是堆区的。
另外还有一个需要注意的地方:
objectivec
static void __main_block_copy_1(struct __main_block_impl_1*dst, struct __main_block_impl_1*src) {
_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}
Block 的这个拷贝函数 __main_block_copy_1
调用了 _Block_object_assign
函数,表示 Block 被拷贝到堆区后,会触发将 __block
变量也拷贝到堆区。仔细看这个 _Block_object_assign
函数,里面 dst 取的是地址,src 取的是指针。dst 之所以要取地址,目的就是在 Block 拷贝到堆区后,要把他的 val
成员属性也变成堆区的:
objectivec
*dest = _Block_byref_copy(object);
到这里,前面的疑惑也就解开了。拷贝到堆区的 Block,它的 val
成员变量也会赋值为拷贝到堆区的 val
。所以不存在当栈区变量释放后,堆区的 Block 调用 val
会引起崩溃的问题。