本章提纲
1、Block对变量的捕获
2、_ _block做了什么?
3、Stack类型的Block是如何变成Malloc类型的?
4、Block的数据结构以及源码分析
5、Block的释放
1.Block对变量的捕获
Block对变量的捕获我们主要来研究四种:一个是对值类型的捕获;加了__block之后的值类型的捕获;对指针类型的捕获;对加了__block的指针类型的捕获。
- 对值类型的捕获
首先实现一个最基本的block,在块中使用局部变量a
。
int main(){
int a = 18;
void(^block)(void) = ^{
printf("lucky - %d",a);
};
block();
return 0;
}
我们通过xcrun命令,编译一下这个文件,看看c++的实现。
可以看到
main
函数中的block
变成了对应的__main_block_impl_0
,并且有三个参数,其中a
也传了进来。block();的调用转化成了
block->FuncPtr
。进一步我们先找到
__main_block_impl_0
的定义:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
通过这个底层的代码,可以看出来Block
的本质是struct
结构体。所以block块的生成实际上是调用它的构造函数。并且在block的内部生成了a
的成员变量,通过函数__main_block_impl_0
参数的方式传了进去进行赋值。
__main_block_func_0
参数1
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
printf("lucky - %d",a);
}
通过这个参数一的实现,可以看到a在这个块中又被copy了一份儿,所以这个内部的块儿中的a
和外部的a
已经不是一个地址了,所以外部修改a不会影响到内部a的值了!。
__main_block_desc_0
结构体指针类型。
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的值类型捕获
接下来我们改一下代码,看看a
加了__block
之后生成的代码变化。
int main(){
__block int a = 18;
void(^block)(void) = ^{
a++;
printf("lucky - %d",a);
};
block();
return 0;
}
代码做如上修改,结果编译结果变成如下:
对比原来的没有添加
__block
的情况,a
在传入__main_block_impl_0
中变成了__Block_byref_a_0
类型的a
,是取a
的地址。而
__Block_byref_a_0
的定义如下:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
它也是个结构体。进一步来看block
编译后的变化:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__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_a_0 *a = __cself->a; // bound by ref
(a->__forwarding->a)++;
printf("lucky - %d",(a->__forwarding->a));
}
__main_block_impl_0
内部的a
此时变成了__Block_byref_a_0 *a;
,不再是int a
了,也就是说,加了__block修饰这个a之后,原来的a被block捕获是a的地址了,并且通过__Block_byref_a_0
来记录。
再来看参数__main_block_func_0
的实现,内部取到a
,是通过a
的__forwarding
再取到a的值进行处理,此时都是地址操作,所以他们最终指向的内存空间是一块儿,就可以在块内
对外部的变量进行操作了。
- 对指针类型的捕获
再修改代码,把a
改成NSObject
然后看看编译之后的结果。
所以block捕获对象也是指针传递,进入内部是可以对值进行修改的,可以看到编译后的底层也是传进去的是指针。
- 加了__block的指针类型的捕获
和值类型一样,也是对应生成了一个__Block_byref_p1_0
,同样也是指针传递。
2._ _block做了什么?
所以根据以上的例子,我们可以总结出来__block
修饰值,或者对象后,相应的会生成一个__Block_byref_x_0
的结构体,而x(a或者是p1)会作为一个成员变量在这个结构体中,读取的时候会通过x->__forwarding->x
去获取值,是通过地址指向的方式拿到对应的值。
所以值类型前边加了__block
,就相当于生成了一个结构体类型,由原来的值传递,变为相应的地址传递!由深拷贝
变成了浅拷贝
。
-
深拷贝与浅拷贝
用一个比喻来简单解释一下深拷贝和浅拷贝的区别,虽然不够准确,但是便于理解。
比如你有一辆宝马x3小汽车,然后你有一把车钥匙。浅拷贝的意思就是,别人来了想管你借车,然后你又搞了一把钥匙给他,这时候你们两个访问的是同一辆车,只要其中一个人在车里做了手脚,比如在里边吃薯片把车里弄的乱七八糟的,等另外一个人进来也会发现里边乱七八糟的,这车坏了受损了,你们两个就谁也操作不了了。而深拷贝就是别人想管你借车,你又去4s店搞了一辆一模一样的车给它,两把钥匙,两辆车,你朋友的车里乱七八糟的不会影响到你开的车,就是两个空间,相互不受影响。但是这样做的劣势也很明显就是浪费了
资源
(内存资源)。除非非常必要的时候,否则系统默认会节省下这部分的资源。所以常见的编译器拷贝大多是浅拷贝。iOS中自定义对象需要进行深拷贝的时候要去自己实现NSCopying协议或者NSMutableCopying,重写方法。
3.Stack类型的Block是如何变成Malloc类型的?
探究这个问题,我们通过运行代码,下符号断点的方式来探究。我们打开符号断点,并在调用block之前打一个断点,block的调用的地方打一个断点。我们看一下截图:
根据汇编可以看到,后面调用
block()
,调用的是objc_retainBlock
。我们进一步下符号断点
objc_retainBlock
来看下它的出处。
再次运行:
看到objc_retainBlock
是在libobjc.A.dylib
库中的_Block_copy
方法。所以这一段实际上执行的是_Block_copy
方法。我们再下符号断点_Block_copy
,切换成真机调试,来看一下寄存器中的block信息,以及_Block_copy
的出处。
刚进入
_Block_copy
方法时,block是stack
类型。我们在_Block_copy
返回处再打个断点调试。也就是说
block
经过方法_Block_copy
就从stack
变为了malloc
类型。下面我们就从源码入手来看看
_Block_copy
的具体操作,从上面的符号断点可以看到_Block_copy
是来自libsystem_blocks.dylib
,但是这个libsystem并没有开源,我们可以用libclosure
替代看一下实现。
4.Block的数据结构以及源码分析
我们打开libclosure,搜索_Block_copy
(添加了注释,省略了预处理部分):
latching_incr_int
的实现
Block的数据结构
Block的类型为Block_layout,是一个结构体:
struct Block_layout {
void * __ptrauth_objc_isa_pointer isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
BlockInvokeFunction invoke;
struct Block_descriptor_1 *descriptor;
// imported variables
};
1、结构体的成员有一个isa
,说明也是一个对象;
2、flag标志位里边包括引用计数,记录block的各种状态,枚举如下(添加少量注释):
// Values for Block_layout->flags to describe block objects 一共占32位 从第0位开始
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime 0x 1111 1111 1111 1110 2^16-2
BLOCK_INLINE_LAYOUT_STRING = (1 << 21), // compiler 2^21
#if BLOCK_SMALL_DESCRIPTOR_SUPPORTED
BLOCK_SMALL_DESCRIPTOR = (1 << 22), // compiler2^22
#endif
BLOCK_IS_NOESCAPE = (1 << 23), // compiler2^23
BLOCK_NEEDS_FREE = (1 << 24), // runtime 2^24
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler 2^25
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code 2^26
BLOCK_IS_GC = (1 << 27), // runtime 2^27
BLOCK_IS_GLOBAL = (1 << 28), // compiler 2^28
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE 2^29
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler 2^30
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler 2^31
};
标志位解析:
- BLOCK_DEALLOCATING,第零位,释放标记,通常与BLOCK_NEEDS_FREE做位与操作,告知该block可以释放。
- BLOCK_REFCOUNT_MASK,引用计数最大值,第一到第十五位表示引用计数值,最大是BLOCK_REFCOUNT_MASK。
- BLOCK_IS_NOESCAPE,第二十三位,是不是可以进行引用计数加减操作。
- BLOCK_NEEDS_FREE,第二十四位,低16位是否有效的标志,程序根据它来决定是否增加或者减少引用计数位的值
- BLOCK_HAS_COPY_DISPOSE,第二十五位,是否拥有拷贝辅助函数
- BLOCK_HAS_CTOR,第二十六位,是否拥有Block的C++析构函数。
- BLOCK_IS_GC,第二十七位,是否开启垃圾回收机制,macOS专有。
- BLOCK_IS_GLOBAL,第二十八位,是否是全局Block。
- BLOCK_USE_STRET,第二十九位,与BLOCK_HAS_SIGNATURE相对,判断是否当前Block拥有一个签名,用于runtime时动态调用。
- BLOCK_HAS_SIGNATURE,第三十位,是否有签名。
前边我们通过lldb调试Block时打印出了Block的签名为:v8@?0,它的含义是:
v
代表返回值类型是void。
8
代表占用的空间。
@?
代表block类型。
0
代表从0号位置开始。
3、reserved预留字段。
4、invoke函数指针,指向Block实现的调用地址。
5、descriptor,附加信息。
源码解析
-
_Block_copy分为三个大的部分,对三种block类型的区分然后分别去进行处理。
1、aBlock->flags & BLOCK_NEEDS_FREE对应的是堆block,我们如果光从block的flag去看的话可能发现不了这个是Malloc类型的block。这个部分我们要结合
3 stackblock
去看,stackBlock直接给了注释,然后根据对stack的操作可以发现,当stack完成了copy后,isa指向了malloc类型,然后标志位置为了BLOCK_NEEDS_FREE
,由此可以推断,这个flag =BLOCK_NEEDS_FREE
时为堆Block。当为MallocBlock类型时,调用了
latching_incr_int
方法,然后直接返回block,所以关键来看latching_incr_int
方法。- latching_incr_int方法解析
内部有两个判断,如果此时标志位为BLOCK_DEALLOCATING,返回BLOCK_REFCOUNT_MASK。
如果OSAtomicCompareAndSwapInt为真,返回old_value+2。这里+2对应+二进制10,引用计数占标志位的1到15位,所以是从第一位开始加,第0位是表示BLOCK_DEALLOCATING
这个状态。
- latching_incr_int方法解析
所以我们可以简单总结出,当为堆Block时
,引用计数+1,然后返回block。
2、aBlock->flags & BLOCK_IS_GLOBAL 当标志位为BLOCK_IS_GLOBAL
,也就是为Global类型的Block时,直接返回Block什么也不做。
3、// Its a stack block.
,stack类型的Block的处理。
- malloc和block同样大小的空间
- 拷贝block的内容到空间中去
- 先清空标志位全部为0
- 设置引用计数为1,设置第二十五位为1也就是BLOCK_NEEDS_FREE为1
- isa指向_NSConcreteMallocBlock
- 返回新创建的block。
4、_Block_call_copy_helper的实现
它的内部通过源码了解,主要调用的方法是_Block_get_copy_function
,所以下面主要是_Block_get_copy_function
的源码解释
而方法_Block_get_copy_function
中的_Block_get_descriptor
的实现主要就是Block_layout
结构体中Block_descriptor_1
的获取。
static inline void *
_Block_get_descriptor(struct Block_layout *aBlock)
{
void *descriptor;
//......
//获取Block_layout中的descriptor的指针
descriptor = (void *)aBlock->descriptor;
//......
return descriptor;
}
我们进一步查看Block_descriptor_1
,然后在Block_descriptor_1
的附近发现了2和3。
Block_descriptor_1、2、3
的实现
Block_descriptor_2
和Block_descriptor_3
的获取
通过以上这些代码可以推断出_Block_call_copy_helper
的过程。
- _Block_call_copy_helper的主要实现方法是
_Block_get_copy_function
-
_Block_get_copy_function
的操作是进行des2的拷贝。- 通过方法
_Block_get_descriptor
获取到Block_layout
结构体中的成员struct Block_descriptor_1 *descriptor;
- 通过内存平移的方式进行平移descriptor的大小,拿到des2。
- 拷贝des2的内容,并且返回。
- 通过方法
- des2的get方法就是通过平移des1内存的大小得到的,这说明在内存布局时,des2紧跟着des1。
- 而des3的获取经过了两步的判断,如果des2存在,那么des3 = des1的首地址+des1的size+des2的size,否则des3= des1首地址+des1的size。
- 再进一步看des1,2,3的实现,发现似曾相识!!!!
- des1内部成员是reserved、size是保留字段和描述block信息大小的size。
- des2内部是copy、dispose 对应标记位为
BLOCK_HAS_COPY_DISPOSE
- des3内部是signature、layout对应的标记为
BLOCK_HAS_SIGNATURE
根据上边的des3的获取方法来看,判断des2是否存在的方法是和标志位BLOCK_HAS_COPY_DISPOSE
进行与操作,所以可以看出来这两个标记为分别对应了des2和des3的内容。通过这两个个标志位就可以简洁的判断出des2和des3是否存在。
在前边我们打印block时也发现有类似的key:signature我们再来看一下。
- 相关参数源码解析
在编译成c++文件后,block还传了几个参数,从编译后的文件可以看到,参数__main_block_desc_0_DATA
和copy
还有dispose
有关系。
__main_block_desc_0_DATA
_main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0),
__main_block_copy_0,
__main_block_dispose_0
};
__main_block_copy_0
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}
__main_block_dispose_0
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}
__main_block_copy_0
调用了方法_Block_object_assign
__main_block_dispose_0
调用了方法_Block_object_dispose
我们上源码中找一下这两个方法的具体实现。
- _Block_object_assign
方法_Block_object_assign
有大量的注释,翻译了一下,虽然翻译的不好,也可能有错误。
注释翻译
:
当一个Block被拷贝到堆上是 可以引用四种不同的类型
- C++ stack based objects c++栈上的基础对象
- References to Objective-C objects 引用oc对象
- Other Blocks 其他的block
- __block variables __block变量
在这些情境下 helper函数 会被编译器通过使用Block_copy和Block_release生成,调用copy和dispose的helpers。copy helper函数向c++堆栈对象发出构造函数调用, 其余会调用运行时来支持函数_Block_object_assign。 在情景一下dispose helper则会调用c++的析构函数并且会调用_Block_object_dispose函数。
这些_Block_object_assign和_Block_object_dispose标志参数的是用来
- BLOCK_FIELD_IS_OBJECT (3), for the case of an Objective-C Object, 3:为了oc对象
- BLOCK_FIELD_IS_BLOCK (7), for the case of another Block, and 7:为了>另外一个Block
- BLOCK_FIELD_IS_BYREF (8), for the case of a __block variable. 8: 为了__block的情况
如果 __block变量被标记为weak,编译器也会是BLOCK_FIELD_IS_WEAK的情况。
所以 block copy或者block dispose helpers 的值只可能是四个 3,7,8,24
当 ——block作为参数,不论是c++对象,或者是一个oc对象,还是另外一个block,编译器都会生成copy、dispose helpers函数。和block copy helper函数相似,__block copy helper函数也会拷贝构造方法,__block dispose helper也会调用析构方法。 同样的 这些helpers函数 都会 调用 这两个带有 相同对象值和额外的带有128位信息支持的Blocks 的相同的支持方法。
所以__block copy或者dispose 的helpers方法 将会分别生成 对象对应的3或者block对应的7,128总是会在信息里边, 下列的组合可能的情况是:__block id 128+3 (0x83)
__block (^Block) 128+7 (0x87)
__weak __block id 128+3+16 (0x93)
__weak __block (^Block) 128+7+16 (0x97)
方法解析
方法_Block_object_assign
的实现逻辑如下:
1、普通对象类型(BLOCK_FIELD_IS_OBJECT),会交给方法_Block_retain_object
处理,也就是arc自动管理。
2、Block作为参数的类型(BLOCK_FIELD_IS_BLOCK),会调用方法_Block_copy。
3、用__block修饰的类型(BLOCK_FIELD_IS_BYREF),调用方法_Block_byref_copy
。
- _Block_object_dispose
而_Block_object_dispose
的实现和_Block_object_assign
是对应的。分别处理了BLOCK_FIELD_IS_BYREF、BLOCK_FIELD_IS_BLOCK、BLOCK_FIELD_IS_OBJECT的情况。
1、BLOCK_FIELD_IS_BYREF情况,调用方法_Block_byref_release
。
2、BLOCK_FIELD_IS_BLOCK情况,调用方法_Block_release
。
3、BLOCK_FIELD_IS_OBJECT的情况,调用方法_Block_release_object
。
这三个方法的解析会在后边的Block释放的部分讲。
- _Block_byref_copy
在方法_Block_object_assign
,被__block修饰的时候,会调用方法_Block_byref_copy
,它的具体实现如下:
static struct Block_byref *_Block_byref_copy(const void *arg) {
//先拿到参数里边的内容,是一个Block_byref结构体对象
struct Block_byref *src = (struct Block_byref *)arg;
// __block 内存是一样 同一个家伙
//引用计数为0
if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
// src points to stack
//开始拷贝参数
//开辟参数一样的大小空间
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
//修改 isa的指向
copy->isa = NULL;
// byref value 4 is logical refcount of 2: one for caller, one for stack
//修改标志位,引用计数修改为2 ,一个是stack引用,一个是调用者引用了 所以是2 或上4是因为要把第0位空过去,第0位是表示是否释放的。
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
//堆上的forwarding指向自己
copy->forwarding = copy; // patch heap copy to point to itself
//原来栈上的那个forwarding指向堆上的
src->forwarding = copy; // patch stack to point to heap copy
//size赋值
copy->size = src->size;
//如果src有copy和dispose helpers
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
//拷贝Block_byref_2中的内容 这个block_bref也分为1,2,3
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;
//如果存在扩展内容 也都拷贝过来 也就是Block_byref_3
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);
}
else {
// Bitwise copy. 直接拷贝 这一次的拷贝中一定包含Block_byref_3
// This copy includes Block_byref_3, if any.
memmove(copy+1, src+1, src->size - sizeof(*src));
}
}
// already copied to heap 如果已经在堆上了 引用计数加1
else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
latching_incr_int(&src->forwarding->flags);
}
return src->forwarding;
}
这个方法主要是对block传进来的参数进行处理,当有__block修饰的情况下,对传进来的Block_byref
进行处理,前边已经对block本身还有它的des处理过了,然后现在是处理__block类型的参数。
1、先进行引用计数的判断,如果引用计数为0,说明之前没有copy过,要进行参数拷贝的操作。
- 开辟空间先拷贝Block_byref中的内容,Block_byref结构体的信息拷贝,栈上的
forwarding
指向改变指到新生成在堆上的这个。 - 判断有没有Block_byref_2,如果有,直接拷贝Block_byref_2。
- 然后判断有没有Block_byref_3,如果有拷贝Block_byref_3。
- 处理byref_keep。
- 如果没有,直接拷贝全部内容,这个里边一定包括Block_byref_3的内容。
2、如果已经存在在堆上了,那么进行引用计数+1,不做别的操作。
5.Block的释放
前边在看方法_Block_object_dispose
时遇到过下列这么三个方法,下边来看下具体实现。
- _Block_release
void _Block_release(const void *arg) {
//获取block
struct Block_layout *aBlock = (struct Block_layout *)arg;
//不存在 啥也不做🤣
if (!aBlock) return;
//全局类型的 啥也不做🤣
if (aBlock->flags & BLOCK_IS_GLOBAL) return;
//不是堆类型 啥也不做🤣
if (! (aBlock->flags & BLOCK_NEEDS_FREE)) return;
//根据dealloc那个标志位 判断是否需要释放
if (latching_decr_int_should_deallocate(&aBlock->flags)) {
//需要释放 调用释放helper
_Block_call_dispose_helper(aBlock);
//析构实例
_Block_destructInstance(aBlock);
//释放内存空间
free(aBlock);
}
}
如果是堆上的block,那么进行dispose_helper调用,调用析构函数,释放空间。
如果不是堆上的,那就啥也不做。
- _Block_byref_release
static void _Block_byref_release(const void *arg) {
struct Block_byref *byref = (struct Block_byref *)arg;
// dereference the forwarding pointer since the compiler isn't doing this anymore (ever?)
//处理forwarding的指针引用计数
byref = byref->forwarding;
//堆类型的
if (byref->flags & BLOCK_BYREF_NEEDS_FREE) {
//获取引用计数
int32_t refcount = byref->flags & BLOCK_REFCOUNT_MASK;
os_assert(refcount);
//判断是否需要释放
if (latching_decr_int_should_deallocate(&byref->flags)) {
//需要释放 并且有Block_byref_2
if (byref->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
//处理Block_byref_2
struct Block_byref_2 *byref2 = (struct Block_byref_2 *)(byref+1);
(*byref2->byref_destroy)(byref);
}
//释放空间
free(byref);
}
}
}
简单总结:
block的拷贝进行三个阶段的拷贝
1、block自身的内容拷贝
2、block的des信息的拷贝,其中要去分别判断des2和des3然后通过内存平移的方式把des2或者des3的内容拷贝过来。
3、block的参数拷贝,当__block修饰的变量被block捕获时,对__block的处理和对des的处理很像,也是一层一层的判断,判断bref_2是否存在,存在拷贝,否则全部momove参数内容,此时的参数内容是一定包含bref_3的。
同样带有__block修饰的参数释放时也是一层一层判断进行析构的。
附一张自己画的流程图: