ブロック原理(1)

ブロックとは正確には、C++コードから始めましょう

最も単純なブロック構造から始めます

image.png

clang -rewrite-objc main.m -o main.cpp && open main.cpp

image.png

image.png

読みやすくするために、コードを単純化しましょう

image.png

さらに読みやすくするために、ここでは名前を簡略化しています。次の簡単なプロセスを参照してください。

image.png

  • clangと組み合わせて、中間のC ++コードをコンパイルし、ブロックを作成して、上の図と組み合わせて、最初にスケッチの輪郭を描きます。

    • 2層構造を作成する

      • BlockCreate構造

      • BlockCreateのメンバーであるブロック構造

    • BlockCreateを介してパラメーターを作成し、BlockCreateメンバーBlock::blockをインスタンス化します

    • 最終的な戻り値はBlockCreate構造体ポインターです

    • BlockCreate構造体の最初のアドレスを介して、メンバーBlock :: blockを取得できます。ブロックは、BlockCreateメモリスペースの先頭にあるため、BlockCreateの最初のアドレスはメンバーブロックの最初のアドレスと同じです。

      最初のアドレス(メンバーブロックアドレス)を取得できるため、メモリオフセットを介してメンバーDescのアドレスを取得することもできます

    • メンバーBlock::blockのアドレスを取得することで、Block :: blockのメンバーメソッドFuncPtrを呼び出すことができます。また、FuncPtrは、Block::blockメンバーがBlockCreateコンストラクトを介してインスタンス化されるときに割り当てられるfun関数のエントリアドレスです。 。

  • この2層構造を必ず理解してください。実際のソースコードではありませんが、後でソースコードを分析することは非常に役立ちます。

前の例では変数を使用していません。前の方法で再度操作して、違いを比較できます。

image.png

image.png

カスタム構造の外部のローカル変数にアクセスする場合

clangによって生成されたc++コードが変更されていることがわかります。上記のインスタンス化プロセスを比較してください。

  • BlockCreate構造体にもう1つのメンバーintがあります

  • BlockCreate構造には、追加のパラメーターもあります

  • 変数aは、func(BlockCreate * self)のBlockCreate :: * selfを介してfunc関数内でアクセスされ、コピーを取得します。

  • 変数aが存在する場所が3つあることがわかります

    • main関数内のローカル変数a

    • BlockCrete 结构体内的成员变量a

    • func方法内部的局部变量a

    其实这3个变量a分别是3个不同的变量了

把局部变量a改为static修饰,继续clang c++查看

image.png

image.png

用static修饰变量a,不一样了

BlockCreate构造传参,此时传递的是 a的地址,而BlockCreate成员 a也变成了 指针, func内部的局部变量a 也变成了 指针,func内部的a是通过 BlockCreate::*self 的指针a 赋值 给func内部的局部变量 指针a

所以static修饰a后,func内部访问的a其实还是 main函数内部的 指针a

把局部变量a改为 __block修饰,继续clang c++查看

image.png

image.png

希望你不会觉得懵,这次复杂了些

  • 出现了一个结构 __Block_byref_a_0

  • BlockCreate 成员Desc的结构内部多了两个 函数 copy & dispose

这里简单解释下

  • 普通的局部变量a 变成了一个结构 __Block_byref_a_0, a是这个结构的成员

    • 成员 void *__isa

    • 成员 __block_byref_a_0 *__forwarding;

    • 成员 int __flags;

    • 成员 int __size

    • 成员 int a

    在main里声明的__block修饰的局部变量, 地址赋值给了 __forwarding, 值赋给了 Block_byref结构里的成员a,注意这个设定, 虽然成员也叫a,只是起到一个接收值的作用,关键在于__forwarding 拿到了原来的a的指针

    先看下__block修饰的a究竟是怎么访问的

image.png

__forwarding 类型 __Block_byref_a_0 *,类似于链表节点,所以也是一个指向 __Block_byref_a_0 结构的指针 至于有什么用,暂存疑,后面源码接着分析

对比着看,其实很明显,不难理解

image.png

block源码 - libclosure-79 查看

源码入口该怎么查看呢,我们先通过汇编看下

image.png

既然retainBlock,说明block开辟了空间,进入查看

image.png

继续跳转 br x16

image.png

目前找到了_Block_copy这样一个符号,然后进入源码查看

image.png

你会看到一个结构Block_layout

image.png

Block_layout 就是前面通过clang c++代码 分析出的 两层结构BlockCreate成员 Block::block

__block 修饰变量 测试代码放进 block源码进行调试

image.png

这段代码是在block源码中测试的

image.png

这其实就是依照Block_layout 栈上的空间结构,在堆区创建了一个Block_layout结构

同时 新开辟的Block_layout结构->invoke 从原来栈上Block_layout->invoke拷贝过来

image.png

既然是堆上开辟空间创建的Block_layout结构,自然isa 指向 _NSConcreteMallocBlock (堆block)

block分析源码遇到问题

现在还有两块没探索到源码,就是 前面通过clang 编译生成的c++代码中__Block_byref_a_0这样的结构,还有一块是BlockCreate构造逻辑部分

那么接下来该何去何从?

我选择最原始的方式 汇编 + 下符号断点 + 结合clang c++代码分析

image.png

先把代码断到此处,防止dyld其他流程干扰

image.png

下符号断点 同时把前面分析过的 _Block_copy 符号也下下来,为了方便分析流程

跟着调试 进入 _Block_object_dispose:

image.png

回到之前clang编译出的c++代码看下

image.png

既然下到了符号_Block_object_dispose 那么同样也把符号 _Block_object_copy下下来继续调试

没有的话 就试试 _Block_object_assign, 之所以没有找到 _Block_object_copy符号,是因为那是由编译器决定的

成功断点符号 _Block_object_assign

image.png

找到头绪,自然我们又回到了源码

image.png

  • 看下源码注释

    When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point to do the assignment.

    当Blocks(可以理解为前面的有成员func的那个结构) 或者 Block_byref持有对象时候,这个入口就会被触发 执行赋值操作

image.png

  • __block int a = 10 类型为 BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK or BLOCK_FIELD_IS_BYREF

    执行 _Block_byref_copy()

_Block_byref_copy

在分析_Block_byref_copy流程之前,我们需要了解下Block_byref 是什么

image.png

从前面clang编译拿到的c++代码,可以看到,Block_byref 是对常规变量的封装,封装结构里还多了isa,__forwarding成员

image.png

源码中还存在 Block_byref_2 Block_byref_3 两个结构,暂且不表,后面会继续说明

我们可以做个假设,目前我们测试的实例 是block引用外部 __block修饰的变量,我们也是这么用的,既然block内部访问外部变量,那么也会对于这个变量的引用计数产生影响 flags就是存储引用计数的

_Block_byref_copy翻译

image.png

如果源byref结构已经在heap上,则不需要执行拷贝,引用计数+1

image.png

中间有一段内存偏移的代码,还没解析,继续

从源码中我们看到

Block_byref_2 *src2 = src + 1
Block_byref_3 *src3 = src2 + 2

那么 Block_byref Block_byref_2 Block_byref_3 是连续的内存结构,根据条件判断是否解析 Block_byref_2 Block_byref_3

认知遗留问题

找遍了源码 clang编译出的c++代码里 __main_block_impl_0 这样的结构并没有发现

image.png

byref_keep byref_destroy 究竟实现了什么功能

因为我们用的常规变量a测试 我们换成object看下

将变量a换为object测试

image.png

clang c++代码

image.png

image.png

从源码得知

image.png

编译阶段,Block_byref结构 flag被设置为 1 << 25, 标识是有 Block_byref_2结构的

image.png

image.png

131有什么意义

image.png

两个参数 + 40 什么意思

image.png

按照编译的逻辑,byref_keep 就是 object类型的对象的 拷贝

但是运行时会做修正 流程有差别

同样 byref_destroy:

image.png

以上为 Block_byref 逻辑,再通过clang得到的c++ 看下 Block_layout 的处理

image.png

image.png

再确认下 __block修饰的 object对象,在block体里 究竟是如何访问的

image.png

总结

  • __block 修饰变量之后,编译器会在栈上构建一个 栈Block_byref(包含变量指针)

  • 定义block,可以理解为编译器生成一个中间结构BlockCreate(这个名字是特意起的,知道是个结构,为了便于理解,你可以这么理解)

    • 同时编译器会在栈上初始化构建一个 栈Block_layout(包含func成员)
  • 执行BlockCreate构造方法

    • 通过Block_layout首地址偏移 得到 Block_copy函数地址, 执行Block_copy,把 栈Block_byref 拷贝 到堆Block_byref

    • 构造参数 栈Block_byref,通过Block_byref首地址偏移 得到 Block_byref_2(包含_Block_byref_copy 即byref拷贝函数)首地址, 执行 _Block_byref_copy函数, 把栈Block_byref 拷贝到 堆Block_byref

    • 继续上一步的位置 内存偏移 8字节,得到堆上开辟的 object内存空间首地址, 这里当然就存放 object对象了

    • 需要注意的一个细节 栈Block_byref 拷贝到 堆Block_byref之后,由于堆上是新的内存空间,那么栈与堆不就两个空间了吗,如何保障访问的是同一块内存?

      解決策は、コピー後にスタックBlock_byrefとヒープBlock_byrefの転送をヒープBlock_byrefにポイントすることです。つまり、ヒープ転送は再びそれ自体をポイントします。

      __blockが変数を変更した後、ブロックブロック内の変数にアクセスする場合でも、ブロック外の変数にアクセスする場合でも、転送を介してヒープスペースにアクセスし、ターゲットスペースの変数にアクセスします。これにより、アクセスされる変数が同じメモリスペース

    image.png

    image.png

image.png

  • Block_byrefが保持する変数のライフサイクルが終了し、_Block_object_disposeを実行します

    • _Block_byref_release関数を実行し、Block_byrefの最初のアドレスのオフセットに従ってBlock_byref_2の最初のアドレスを見つけ、byref_destroyを取得するために8バイトをオフセットし続けます。デストラクタを実行してヒープメモリスペースを再利用します。
  • Block_layoutスコープが終了するか、ライフサイクルが終了し、_Block_releaseを実行します

    • Block_layoutの最初のアドレスオフセットに従ってBlock_descriptor_2の最初のアドレスを見つけ、8バイトのオフセットを続けてから、disposeはデストラクタを実行して、ヒープ上に開かれたBlock_layoutヒープメモリスペースを再利用します。

レジスタビューの読み取り

image.png

シンボリックブレークポイント_Block_copy

image.png

_Block_copyが実行される前に、レジスタraxはパラメータを受け取ります(arm64はレジスタx1を読み取ります)

image.png

実行後、retは戻り、raxレジスタは戻り値を格納します

image.png

  • 変数aが__block変更に変更されます

image.png

__blockの変更により、コピー機能のアドレスがBlock_layoutに表示され、コピーを通じて_Block_copyが実行されます。

__blockを変更しないと、コピー破棄機能はなく、デフォルトで_Block_copyが実行されます。

この違いは、パラメータを作成するときのフラグの違いが原因です。__blockの変更前は、0であり、__ blockの変更後は、1 << 25

image.png

おすすめ

転載: juejin.im/post/7118386172414328868