前言
在上一篇《Block基础探索》中探索了block的分类和循环引用的处理。在这一过程中,还有一些问题我们不甚了解,例如block底层是一个结构体,这个结构体是什么样的结构?block是怎样捕获的self,又是如何捕获变量的?本篇我们从block源码来继续探索下。
一、Block底层结构
block是什么?是匿名函数?或者是一个对象?但是好像都不对。还记得上一篇中探索block循环引用的原因时,使用过xcrun查看了底层,本节继续使用这种方法来看下。
在探索前,先分析下block的几种形式,再一一看下底层实现。根据使用block的情况看,block的形式可以分为以下几种:
- 有参数
- 有返回值
- 有参数也有返回值
- 无参数也无返回值
由于第三种包含前两种,所以实际上我们
xcrun两次即可,两次的代码和结果分别如下:
1、有参数有返回值
int(^block)(int a) = ^int(int a){
return 0;
};
2、无参数无返回值
void(^block)(void) = ^{
};
对比两个结果,当有参数和返回值时,__ViewController__viewDidLoad_block_func_0也有对应类型的参数和返回值,否则就没有参数和返回值;而两种情况下,两者的__ViewController__viewDidLoad_block_impl_0并没有差别。因此可以得出结论,block的参数和返回值影响的只是代码块的实现,而不会影响block本身。
block除了参数和返回值,还会捕获外部变量,根据捕获变量的情况,又可以分为以下几种:
- 捕获基本类型变量
- 捕获对象类型变量
- 通过
__block捕获基本类型变量 - 通过
__block捕获对象类型变量
下面以是否使用__block修饰分为两组进行xcrun,因为参数和返回值对于block本身结构没有影响,故这里采用无参数无返回值的block进行实验。
捕获不用__block修饰的变量
1、基本类型
int a = 10;
void(^block)(void) = ^{
NSLog(@"value = %d", a);
};
block的底层是一个结构体,其中包含一个int a成员,在其调用的代码块函数中,还有一个局部变量int a接收外部传入的值,而这个值正是int a = 10的值。由此可以发现,block代码块中的a和ViewDidLoad中的a已经不是同一个变量。
如果直接在block中修改a的值,我们预期的是改变外部的a,但实际是block内部a的改变,由此产生了代码歧义。所以,苹果并不允许直接在block中修改外部的局部变量,如果要改的话,需要加上__block,这一点后面也会分析到。
2、对象类型
NSObject *obj = [NSObject alloc];
void(^block)(void) = ^{
NSLog(@"value = %@", obj);
};
与捕获基本类型相比,捕获对象多了两个函数__ViewController__viewDidLoad_block_copy_0和__ViewController__viewDidLoad_block_dispose_0,这两个函数分别调用了_Block_object_assign和_Block_object_dispose,其实是用来持有和释放对象的,这在后面也会得到验证。
除了上面的不同外,还有一点就是在block结构体中的成员变量变成了一个对象所属类型指针变量,其值为外部传入的对象。与基本类型相同的是,在代码块函数中接收的也是外部传入的对象。
不过需要区分一点,对于对象类型,在block内部修改外部局部变量,由于指针指向同一片内存,修改变量的成员或者属性,是可以修改的,但是如果是将变量重新赋值一个新的对象,则也是不被允许的,这一点的原因和基本类型一致。
捕获__block修饰的变量
1、基本类型
__block int a = 10;
void(^block)(void) = ^{
NSLog(@"value = %d", a);
};
加了__block的基本类型变量,在被捕获时,会多出一个__Block_byref_obj_0类型的结构体,并且在ViewDidLoad方法中,先将int a = 10包装成了__Block_byref_obj_0,其中__forwarding为a的地址。
在block的结构体中,成员a不再是int类型,而是一个__Block_byref_obj_0结构体指针,在结构体的构造函数中传入包装a的__Block_byref_obj_0结构体的地址。因为是指针传递,所以block捕获到的a,与外部的a是同一块内存,当block内部改变时,外部的a也会发生改变,这样做不会产生如普通基本类型的代码歧义。
此外,加了__block后还多了copy和dispose两个函数,这两个函数的作用是什么呢,下文会继续探索。
2、对象类型
__block NSObject *obj = [NSObject alloc];
void(^block)(void) = ^{
obj = [NSObject alloc];
NSLog(@"value = %@", obj);
};
加了__block后的对象类型,也会多一个__Block_byref_obj_0类型的结构体,这一点和基本类型一致。这个例子中,专门在block中专门给obj重新赋值,查看其内部实现逻辑为:
- 将外部对象的指针地址传入
__Block_byref_obj_0中,并将__Block_byref_obj_0的地址传入block中 - 在代码块函数
__ViewController__viewDidLoad_block_func_0中,有代码__Block_byref_obj_0 *obj = __cself->obj;,通过block结构体的构造函数和这一步操作,将外部对象的指针传入block内部 - 因此,在block内部重新赋值时,并不会报错,因为内外指针一致,会一起修改。
需要注意的是,无论是否使用__block修饰,当捕获的变量是对象时,在block内部都是可以修改对象的成员和属性的,并且内外值是同时改变的,因为两者都是操作同一片内存。不同点在于:
- 如果不使用__block修饰,是通过两个指针指向同一片内存,所以修改对象成员和属性是被允许的,内外的修改是一致的,但是在block内部重新给对戏那个赋值,则改变了内部指针的指向,对外部没有影响,从而产生歧义,不被允许;
- 而使用__block修饰,则是将外部对象的指针传进block中,实际是一个指针,因而在block内部重新赋值后,还可以保证内外指向的是同一片区域
小结
本小节主要探索的是block的本质和捕获变量的底层实现,这里做下总结:
- 1、block的底层是一个
结构体,并且该结构体成员不包含block的参数和返回值,参数和返回值只是作为代码块函数的参数和返回值 - 2、未使用__block修饰的外部变量,如果是基本类型,则是值传递,block内外是两个变量,如果是对象类型,则是指针传递,block内外是两个指针指向同一变量,可以修改对象的成员和属性,但是不能在内部给对象重新赋值
- 3、使用__block修饰的外部变量被捕获时,都会被包装成一个
__Block_byref_obj_0结构体,将外部变量的指针传入block内部,因此即使修改指针的指向,内外变量也可以保持一致,不会歧义
不过,还有些问题没有清楚,
- block如何进行拷贝
- 对象类型的copy和销毁函数,是如何copy和销毁对象的 不要着急,在下一节中,将结合源码和clang的结果继续进行探索。
二、block源码探索
在上面的探索中,block的isa都指向为&_NSConcreteStackBlock,但是上面的几个例子中的block,按道理应该属于堆block,难道是之前的判断方法不正确?其实不然,因为上述例子是在编译时,此时还不能确定block类型,因此用&_NSConcreteStackBlock占位。
在上面的例子中打下断点,并开启汇编模式Always show Disassembly,如图所示:
之后会进入汇编代码界面:
如上图,下一个执行的跳转函数为objc_retainBlock,接着单步执行进入下一函数
在该函数的最后ret指令处打断点,然后register read x0读取返回值,得到如下结果:
可以发现在经过__Block_copy函数后,block变成了堆block,在这个过程中一定经历了一些操作,我们可以在__Block_copy函数中一探究竟。
从上面的结果可以发现,__Block_copy函数在libsystem_blocks.dylib库中,但是该库并未开源,不过有一个替代库libclosure可供研究,其源码地址为libclosure。
打开libclosure源码,全局搜索__Block_copy,发现其返回值是一个Block_layout结构体,源码定义代码如下:
struct Block_layout {
void * __ptrauth_objc_isa_pointer isa;
volatile int32_t flags; // contains ref count //在后面的方法中会用到,用于跟枚举做逻辑&运算
int32_t reserved;
BlockInvokeFunction invoke; // block代码块函数,调用block要执行的代码
struct Block_descriptor_1 *descriptor;
// imported variables
};
这里Block_descriptor_1后面是一个导入变量,根据不同的情况决定是否会导入,像这样的变量有三种类型,分别为Block_descriptor_1、Block_descriptor_2、Block_descriptor_3,源码中定义如下:
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
uintptr_t reserved; // 保留字段
uintptr_t size; // block结构体的总大小,可以参考上节例子cpp文件中__ViewController__viewDidLoad_block_desc_0构造函数中的sizeof
};
#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
BlockCopyFunction copy; // 外部传入的copy函数
BlockDisposeFunction dispose; // 外部传入的dispose函数,用于销毁变量
}; // 当捕获对象类型或__block修饰的基本类型时,才会有Block_descriptor_2
#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature; // block的签名,例如汇编查看返回值时的"v8@?0"
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
flag成员表示block的状态,有一个枚举来表示,代码如下:
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
BLOCK_INLINE_LAYOUT_STRING = (1 << 21), // compiler
#if BLOCK_SMALL_DESCRIPTOR_SUPPORTED
BLOCK_SMALL_DESCRIPTOR = (1 << 22), // compiler
#endif
BLOCK_IS_NOESCAPE = (1 << 23), // compiler
BLOCK_NEEDS_FREE = (1 << 24), // runtime
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code
BLOCK_IS_GC = (1 << 27), // runtime
BLOCK_IS_GLOBAL = (1 << 28), // compiler
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler
};
三、block捕获变量的过程分析
3.1 block拷贝 堆->栈
上一小节看到了block的源码结构是一个Block_layout结构体,现在回到__Block_copy函数中来,在源码中函数的实现如下所示:
全局搜索__Block_copy函数的调用,可以发现两个调用地方
第一个是__NSMallocBlock__的retain方法中,此时是将block拷贝到堆上,这是block的第一层拷贝。
第二个是_Block_object_assign函数中,这是捕获了外部的一个block变量做的拷贝。
结合__Block_copy函数源码及其调用时机分析函数实现流程如下:
- 如果是一个全局block,即
aBlock->flags & BLOCK_IS_GLOBAL则直接返回aBlock。 - 如果不是全局blcok,则进行拷贝,将blcok从栈拷贝到堆,并且将isa改为
_NSConcreteMallocBlock,同时result->flags |= BLOCK_NEEDS_FREE | 2;,再一次进入时,flag已经包含BLOCK_NEEDS_FREE,也就不会再copy一次。
3.2 block拷贝外部变量
上一小节__Block_copy函数将block从栈拷贝到堆,这是block的第一层拷贝。本小节看一下block的第二层拷贝 -- 对于外部变量的拷贝。
对外部变量的拷贝,分四种情况:
- 1、不加__block的基本类型
- 2、不加__block的对象类型
- 3、有__block修饰的基本类型
- 4、有__block修饰的对象类型
3.2.1 不加__block的基本类型
第一种很简单,只是一个值拷贝,内外是两个值相同的不同变量,并且因为是基本类型,所以不需要考虑释放的问题。
3.2.2 不加__block的对象类型
第二种是对象类型,所以结合源码来分析下。在第一节的例子中,通过xcrun得到的cpp代码实现中有一个__ViewController__viewDidLoad_block_desc_0结构体,该结构体其实是Block_descriptor_1、Block_descriptor_2三者的合体,可以对比看下:
static struct __ViewController__viewDidLoad_block_desc_0 {
size_t reserved; // => 对应Block_descriptor_1的reserved
size_t Block_size; // => 对应Block_descriptor_1的size
// => 对应Block_descriptor_2的copy
void (*copy)(struct __ViewController__viewDidLoad_block_impl_0*, struct __ViewController__viewDidLoad_block_impl_0*);
void (*dispose)(struct __ViewController__viewDidLoad_block_impl_0*); // => 对应Block_descriptor_2的dispose
}
其中copy和dispose的赋值代码如下:
copy和dispose两者传入的值为__ViewController__viewDidLoad_block_copy_0和__ViewController__viewDidLoad_block_dispose_0,在这两个函数中分别调用_Block_object_assign和_Block_object_dispose。
首先在源码中搜索_Block_object_assign,发现如下实现:
该函数看起来很长,但是内部其实只有一个switch分支。分支的条件由flags确定,flags来源于以下枚举:
// Runtime support functions used by compiler when generating copy/dispose helpers
// Values for _Block_object_assign() and _Block_object_dispose() parameters
enum {
// see function implementation for a more complete description of these fields and combinations
BLOCK_FIELD_IS_OBJECT = 3, // id, NSObject, __attribute__((NSObject)), block, ...
BLOCK_FIELD_IS_BLOCK = 7, // a block variable
BLOCK_FIELD_IS_BYREF = 8, // the on stack structure holding the __block variable
BLOCK_FIELD_IS_WEAK = 16, // declared __weak, only used in byref copy helpers
BLOCK_BYREF_CALLER = 128, // called from __block (byref) copy/dispose support routines.
};
捕获不加__block的对象类型的情况下,根据cpp文件的参数,传入的是flags值为3,即BLOCK_FIELD_IS_OBJECT,因此会调用_Block_retain_object函数,该函数在源码中的定义为:
static void (*_Block_retain_object)(const void *ptr) = _Block_retain_object_default;
static void _Block_retain_object_default(const void *ptr __unused) { }
在源码中搜索_Block_object_dispose,代码如下:
进入BLOCK_FIELD_IS_OBJECT分支下的_Block_release_object函数,发现其定义如下:
static void (*_Block_release_object)(const void *ptr) = _Block_release_object_default;
static void _Block_release_object_default(const void *ptr __unused) { }
结合两个函数,发现其实BLOCK_FIELD_IS_OBJECT下copy和dispose什么都没做,也就是说捕获不加__block的对象类型时,外部传入的变量并没有额外的操作,其实是交给ARC来处理了。
3.2.3 有__block修饰的变量
有__block修饰的对象被捕获时,会将其包装成一个__Block_byref_obj_0结构体,在cpp文件中定义为:
struct __Block_byref_obj_0 {
void *__isa;
__Block_byref_obj_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *obj;
};
这是block的第二次拷贝,即将外部对象拷贝到block内,并包装成__Block_byref_obj_0结构体,其包装代码为:
__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 *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"),sel_registerName("alloc"))};
copy和dispose如下:
同样是调用了_Block_object_assign和_Block_object_dispose函数,不过flags传的值为8,即BLOCK_FIELD_IS_BYREF。
查看_Block_object_assign,发现此时调用的是_Block_byref_copy函数,进入该函数定义处:
该函数的返回值是一个Block_byref结构体,源码中定义为:
struct Block_byref {
void * __ptrauth_objc_isa_pointer isa; // 8
struct Block_byref *forwarding; // 8 用于存储外部变量的指针
volatile int32_t flags; // contains ref count//4
uint32_t size; // 4
};
struct Block_byref_2 {
// requires BLOCK_BYREF_HAS_COPY_DISPOSE
BlockByrefKeepFunction byref_keep; //= __Block_byref_id_object_copy_131
BlockByrefDestroyFunction byref_destroy; // = __Block_byref_id_object_dispose_131
};
struct Block_byref_3 {
// requires BLOCK_BYREF_LAYOUT_EXTENDED
const char *layout;
};
可以发现,原来cpp文件中的__Block_byref_obj_0就是Block_byref、Block_byref_2、Block_byref_3三者的组合。
继续回到源码_Block_byref_copy函数,该函数会接受一个arg参数,其实是包装成Block_byref指针的外部变量,并赋值给Block_byref *类型的变量src。
当判断(src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0时,要进行block的第三层拷贝,会开辟一个新的Block_byref *变量copy,并将src相关的值赋给该变量。见如下代码:
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;
这里可以看到,src和copy的forwarding都指向了copy,而copy也是一个指针,当操作copy指针修改其指向内存的内容时,src和copy都会发生改变。
如果src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE有值,即外部传入了copy和dispose,此时还需要拷贝copy和dispose,并调用byref_keep函数,代码如下:
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 (src->flags & 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);
与Block_descriptor_2中的不同,这里传入的copy和dispose是存储在Block_byref_2中的,对应cpp中的代码为:
注意这里的 dst+40 和 src+40,这实际上是做了内存平移,对照Block_byref、Block_byref_2、Block_byref_3的定义,从Block_byref首地址开始,偏移40,最终找到layout,实际存储的就是外部变量。
而此时传入的flag为131,即BLOCK_FIELD_IS_OBJECT | BLOCK_BYREF_CALLER,对比_Block_object_assign和_Block_object_dispose源码,发现在该分支下copy做了赋值,将src持有的对象拷贝到copy上,dispose则什么都没做,其实也是交给了ARC来进行内存管理。
上面是__block修饰的对象类型,如果是基本类型,也会包装成Block_byref,不过其flags传入为0,因此在_Block_object_assign和_Block_object_dispose调用时,没有匹配分支,不会进行操作,也不用考虑基本类型的释放问题。
_block的变量拷贝问题比较复杂,这里稍微总结一下
- 1、调用
__Block_copy将block从栈拷贝到堆,这是block的第一层拷贝 - 2、将外部变量拷贝到
block_byref中,然后调用_Block_byref_copy,这是block的第二层拷贝 - 3、在
_Block_byref_copy内部调用block_byref_2的copy函数,将内部的变量进行拷贝,这是block的第三层拷贝
总结
本文结合源码和xcrun,主要探索了block的底层原理,总结起来有如下几点:
- 1、block底层是一个结构体,结构体成员因是否捕获变量而改变
- 2、捕获的变量不是__block修饰时,如果是基本类型最基本的block拷贝到堆上这一次拷贝,如果是对象类型,则需要拷贝外部变量到block中
- 3、如果是__block修饰的变量,会进行三层拷贝,具体可见
3.2.3小节
以上即为对于block底层的探索,结合上一篇的blcok基础探索,完善了关于block总结。本篇文章的探索还是有些许绕,感谢您能看到最后,也欢迎提出不同意见。