@[TOC](iOS 底层探索篇 —— block(下))
block底层源码
block 编译成一个什么样的结构
block invoke-> isa->签名 ->捕获 -> 保存 -> 释放
1. block底层分析
要探究block底层是什么样的一个结构,那么就定义一个block,然后xcrun 一下。
xcrun之后,打开生成的cpp文件,看到生成的main函数。
把函数的类型强转去掉之后得到下面的代码。这里看到 void(*block)(void) 等于__main_block_impl_0函数的返回值,而block调用等于block->FuncPtr(block)。
搜索一下__main_block_impl_0,那么就说明block等于这个结构体,之前的方法是这个结构体的构造函数。在main函数中定义了一个int a,而结构体这里也有一个int a,两者之间有什么联系呢?
这里试着把block里面的a去掉,然后重新xcrun一下。这里看到结构体里的a消失了。并且构造函数里面的参数以及a(_a)消失了。a(_a)是一个c++的一个语法,会讲传进来的参数赋值给成员变量a。
那么也就是说:block在底层捕获了a,并形成了相对应的成员变量。
把int 换成NSObject试一下,发现确实形成了相对应的成员变量。(这里用的NSObject,所以换到viewController文件中,重新xcrun了)。
看到block的结构体,注意到这里的isa指向的是_NSConcreteStackBlock,这里捕获到了外界变量,为什么还是stackBlock呢?那么是不是编译时的时候是_NSConcreteStackBlock,到运行时的时候动态变成了mallocBlock的呢?
同时注意到这里传进来的参数fp,赋值给了impl的FuncPtr。fp是 __main_block_func_0,那么也就是说 impl.FuncPtr = __main_block_func_0。搜索__main_block_func_0,发现里面是函数实现。那么也就是说funcPtr里面存的是block的函数实现。而之前的 block->FuncPtr(block);其实也就是调用了__main_block_func_0。
这里说明了block对fp进行了函数式保存。这里看到int a = __cself->a,__cself是传进来的参数也就是block自身,那么__cself->a也就是block结构体的成员变量a。这里的int a 进行了值拷贝。
2. __ block 的作用
在声明a的时候,添加__block修饰。
重新xcrun看到结构体和函数里的int a变成了 __Block_byref_a_0 a。
再来看到main函数,这里的a的结构也变成了__Block_byref_a_0结构体,并且进行了初始化。并且这里保存的是&a也就是a的地址。这里省略了int a = 18 的步骤,&a就是这个a的地址。
接下来找到__Block_byref_a_0结构体,这里的__forwarding也就是a的地址。
这样block里面保存的就是a的地址,__Block_byref_a_0 *a 等于__cself->a 也就是a的地址。这个时候,__main_block_func_0函数内部的a,就和main函数的a相同了,因为他们的地址是一样的,指向同一片内存空间。这个时候,在block内部修改a的值,外部的a的值也会修改了。
所以,__block的作用是生成了__Block_byref_a_0结构体,并且是将指针地址传给了block,这样就能达到修改同一片内存空间的效果。
3. block底层copy处理
打下断点后运行下面的代码。
发现这里有调用一个objc_retainBlock,然后走到objc_retainBlock里面。
也可以通过下符号断点的方式来进入objc_retainBlock。
接下来去libobjc源码中搜索objc_retainBlock,发现里面调用了_Block_copy。在libobjc中没有搜到_Block_copy,那么就去下符号断点。
发现在libsystem_blocks.dylib里面,这个框架不是开源的。这里可以用反汇编来进行分析,也可以通过替代工程libclosure79来分析。
来到libclosure79里搜索_Block_copy。这里看到block真正从底层结构Block_layout。
Block_layout里面的成员变量有:
-
isa:指向表明block类型的类
-
flags:标识符,按bit位表示一些block的附加信息,类似于isa中的位域,其中flags的种类有以下几种,主要重点关注BLOCK_HAS_COPY_DISPOSE 和 BLOCK_HAS_SIGNATURE。 BLOCK_HAS_COPY_DISPOSE 决定是否有 Block_descriptor_2。BLOCK_HAS_SIGNATURE 决定是否有 Block_descriptor_3
-
reserved:保留信息,可以理解预留位置,猜测是用于存储block内部变量信息
-
invoke:是一个函数指针,指向block的执行代码
-
descriptor:block的附加信息,里面有很多内容,比如是否正在析构、是否有析构函数,是否有keep函数等。有三类: Block_descriptor_1是一定存在的,Block_descriptor_2 和 Block_descriptor_3都是可选参数。
接下来去看一下flags里面是什么内容。看到有以下这些标识符。这里主要重点关注BLOCK_HAS_COPY_DISPOSE 和 BLOCK_HAS_SIGNATURE。 BLOCK_HAS_COPY_DISPOSE 决定是否有 Block_descriptor_2。BLOCK_HAS_SIGNATURE 决定是否有 Block_descriptor_3
回到block copy 方法读取寄存器,看到这里消息接收者是 NSGlobalBlock类型。
回到viewController让block捕获外界变量后重新运行。
重新读取寄存器后输出,发现是NSStackBlock,并且多了copy和dispose。这里的block应该是MallocBlock,为什么还是NSStackBlock呢?猜想是在copy里面做了处理,将NSStackBlock变为MallocBlock。
在copy 方法 return的地方打下断点,运行过去后重新读取寄存器,发现果然变成了NSMallocBlock类型。
接下来去看源码是如何操作的。
首先这里会判断block的状态,如果是BLOCK_NEEDS_FREE 或者BLOCK_IS_GLOBAL 就不继续下面的操作,直接返回aBlock。
如果两者都不是,那么就会对block进行处理。这里是编译期过来的,所以只能是栈block,也就是stackBlock。因为如果在编译期进行内存开辟的话,那么对编译器的压力太大,所以在编译时都标记为栈block。而到了运行时,发现这个栈block还捕获了外界的变量,那么就会根据这个block copy一份,并且将其isa改为_NSConcreteMallocBlock。这里的操作是拿到block的大小,然后根据这个大小开辟一个新的内存空间,然后调用memmove进行拷贝,然后拷贝invoke等其他属性,并且对标识符进行处理,最后才改变block的isa。所以_Block_copy操作之后,就会把栈block变为堆block。
之前读取寄存器的时候,发现其还打印出了 signature: “v8@?0”,这个就是block的签名。打印一下。有了签名,那么就可以做相对应的hook的处理。block的invoke其实也是消息发送,如果消息失效或者发生问题,那么就会进入消息转发流程。在消息转发的慢速转发流程过程中,必须要用到签名才能进行相关的处理。
之前的分析中,Block_layout还有一个descriptor,descriptor里面有什么呢?发现里面装着reserved和size,那么读取寄存器时候打印出来的copy 和dispose 存在哪里呢?
这里看到,copy 和 dispose 在Block_descriptor_2里面。这里涉及到了内存的连续可选。Block的类型 不一样,那么其相对应的结构也就不一样,所以这里通过标识符来判断内存的可选。这里的标识符就是BLOCK_DESCRIPTOR_2 和 BLOCK_DESCRIPTOR_3。
那么标识符是由什么来决定的呢?在setter里面没有搜到有用的信息,那么就从对应的getter方法来查找。搜索Block_descriptor_2,看到这里的getter。这里要拿到_Block_descriptor_2或者_Block_descriptor_3,都要 += sizeof(struct Block_descriptor_1) ,那么也就是说Block_descriptor_1是一定存在的,而_Block_descriptor_3则还需要通过aBlock->flags & BLOCK_HAS_COPY_DISPOSE来判断是否存在Block_descriptor_2来决定是否需要进行 += sizeof(struct Block_descriptor_2) 的操作。
之前打印出来的堆block这里有copy,dispose 和 signature,那么也就是说,这个堆block里面有Block_descriptor_2和_Block_descriptor_3。
既然是内存平移得到的,那么我们就可以通过内存平移得到Block_descriptor_2和_Block_descriptor_3。这里isa 8 个 字节, int32_t 4个字节, 那么 flags 和 reserved 就是8个字节。invoke 8个字节
x/8gx 打印一下MallocBlock,发现果然平移16位后为invoke。
descriptor是一个Block_descriptor_1* 类型,也就是8字节,所以0x00000001006c40d0就是descriptor。
x/8gx 打印出descriptor,那么前面16个字节就是存的descriptor1,往后16个字节存descriptor2,再往后16个字节存的是descriptor3。而0x00000001006c1308和0x00000001006c1344也确实是之前打印出来的copy和dispose的地址。打印0x00000001006c3477也打印出了签名。
_Block_object_assign
这里看到有个__ViewController__viewDidLoad_block_desc_0结构体,其默认等于__ViewController__viewDidLoad_block_desc_0_DATA,也就是说里面的copy等于__ViewController__viewDidLoad_block_copy_0,dispose等于__ViewController__viewDidLoad_block_dispose_0。而这里的copy和dispose都是descriptor2里面的东西。
看到__ViewController__viewDidLoad_block_dispose_0方法,发现这里面调用了_Block_object_assign这个方法,这个方法是做什么的呢?
在源码中搜索_Block_object_assign,发现这样一段注释。_Block_object_assign和_Block_object_dispose里面的flags将会根据对象的不同而设置不同的值。
- 普通对象,即没有其他的引用类型 BLOCK_FIELD_IS_OBJECT (3)
- block类型作为变量 BLOCK_FIELD_IS_BLOCK (7)
- 经过__block修饰的变量 BLOCK_FIELD_IS_BYREF (8)