这是我参与8月更文挑战的第27天,活动详情查看:8月更文挑战
block底层源码
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
来分析。
来到libclosure7
里搜索_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
也打印出了签名
。
4. _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_copy_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)
回去将变量设为普通对象后重新xcrun,发现确实变成了3。
继续在源码中搜索_Block_object_assign
,找到了实现。这里看到如果是普通的对象类型,那么就会进行_Block_retain_object
的操作。然后拷贝对象的值,指向同一片内存空间。
查找_Block_retain_object
,发现等于_Block_retain_object_default
,而_Block_retain_object_default是空实现
。那么这个代表什么呢?这里其实是交给了系统级别的ARC进行相关的处理
。
如果是block类型
的变量,则通过_Block_copy
操作,将block从栈区拷贝到堆区
如果是__block
修饰的变量,调用_Block_byref_copy
函数 进行内存拷贝
以及常规处理
接下来搜索_Block_byref_copy
函数。这里进行了判断,确保对象是有引用计数
的,然后让ARC进行处理。这里根据原来的对象开辟内存空间
,然后进行相对应的copy处理
,并且将两者的forwarding都指向copy
。往下还看到这里判断Block是否有copy
和dispose
, 有的话则进行一系列拷贝的工作,然后调用src2的 byref_keep
方法,对对象的生命周期做了保存
。
src的copy
和dispose
函数,是结构体的成员,而变量在声明的时候,传进来了这两个参数
,所以block_byref_2 的 copy
等于 __Block_byref_id_object_copy_131
,而destroy
等于__Block_byref_id_object_dispose_131
。
搜索一下__Block_byref_id_object_copy_131
,发现又调用了一次_Block_object_assign
。这里的 + 40
是将通过内存平移
拿到__Block_byref_objc1_0
结构体里面的objc1
,也就是viewDidLoad里面创建的那个变量,在进行一次_Block_object_assign
来进行常规的引用计数处理
和变量赋值
。
__block三层copy:
- block copy 将block从栈拷贝到堆上面
- block进行捕获变量 block_byref,然后copy block_byref
- block_byref对object进行变量的copy