iOS 底层探索篇 ——block(下)

427 阅读9分钟

这是我参与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的FuncPtrfp__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里面的成员变量有:

  1. isa:指向表明block类型的类

  2. 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

  3. reserved:保留信息,可以理解预留位置,猜测是用于存储block内部变量信息

  4. invoke:是一个函数指针,指向block的执行代码

  5. 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_2BLOCK_HAS_SIGNATURE 决定是否有 Block_descriptor_3

在这里插入图片描述

回到block copy 方法读取寄存器,看到这里消息接收者是 NSGlobalBlock类型。

在这里插入图片描述

回到viewController让block捕获外界变量后重新运行。

在这里插入图片描述

重新读取寄存器后输出,发现是NSStackBlock,并且多了copydispose。这里的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里面有什么呢?发现里面装着reservedsize,那么读取寄存器时候打印出来的copydispose 存在哪里呢?

在这里插入图片描述

这里看到,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这里有copydisposesignature,那么也就是说,这个堆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就是descriptorx/8gx 打印出descriptor,那么前面16个字节就是存的descriptor1往后16个字节descriptor2再往后16个字节存的是descriptor3。而0x00000001006c13080x00000001006c1344也确实是之前打印出来的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是否有copydispose, 有的话则进行一系列拷贝的工作,然后调用src2的 byref_keep方法,对对象的生命周期做了保存

在这里插入图片描述

src的copydispose函数,是结构体的成员,而变量在声明的时候,传进来了这两个参数,所以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:

  1. block copy 将block从栈拷贝到堆上面
  2. block进行捕获变量 block_byref,然后copy block_byref
  3. block_byref对object进行变量的copy