- block的底层浅分析

650 阅读4分钟

block的分类

image.png

  • 创建一个block.c文件
#include "stdio.h"

int main(){

    int a = 18;
    void(^block)(void) = ^{
        printf("LG_Cooci - %d",a);
    };
    
     block();
    return 0;
}

  • 打开终端进入当前文件路径中
  • 输入指令 xcrun -sdk iphonesimulator clang -S -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.4 block.c
  • 生成一个同名的.cpp文件

image.png

  • 这段就是对应的block的代码 image.png

  • 这个就是block的结构体 image.png

  • 这里发现有一个int a,这是以为这个block捕获了外界的变量才会生成的,如果没有捕获外界的成员变量,这个就是没有的。

  • __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) 函数这里 : a(_a)就比较奇怪这里其实就是完成了a = _a,的赋值操作。

  • isa = 是一个栈block,因为这里还是编译阶段,没有真正完成运行,所以这里的栈block是如果成为堆block的需要研究一下

  • 函数的存储以及调用 image.png

  • 1.一开始这里有个函数方法__main_block_func_0

  • 2.block创建时会调用方法__main_block_impl_0把上一个函数方法作为第一个参数传入进来,参数名为pt,保存impl.FuncPtr = fp;,调用block)->FuncPtr这里就是block开始调用了刚刚存放起来的函数方法。

  • 3.block调用__main_block_func_0传入一个参数__cself这个就是block,int a = __cself->a; // bound by copy这里就是取出block中的a值进行临时变量的赋值,打印。

__block 的作用分析

  • 添加__block image.png

  • 重新编译查看 image.png 发现类型发生了变化。int a 变成了 __Block_byref_a_0 *a

  • 这里就是取a的内存地址,完成结构体的初始化。结构体第一个值为0,第二个值为 a的内存地址,第三个为18个长度的内存大小。 image.png

  • *__forwarding存放了 a的内存地址。 image.png

  • block创建的时候把机构体 __Block_byref_a_0 a 的地址传入进去进行保存 image.png

  • 这里就是a = a的地址 forwarding image.png

  • 当调用block取值的时候,取出的值a就是取的地址,地址赋值后,临时变量地址和原来相同,指针赋值。 image.png

结论:__block的作用是生成了一个__Block_byref_a_0结构体让block捕获成员变量的地址值,而不是简简单单的值,放block不会变量的地址值。完成修改同一个内存地址的作用。

测试调试流程

  • 写一个简单的block image.png

  • 打开汇编调试 image.png

  • 发现这里进入了objc_retainBlock image.png

  • 进入的时block_copy image.png

底层源码查看 libclosure-79

  • 1.block的底层结构是这个结构体 image.png
    • 1.isa 是 isa的指向表明block的类型
    • 2.flags是一个标识
    • 3.reserver是流程数据
    • 4.invoke 存放调用函数
    • 5.descriptor 其他的相关的描述
  • 判断是否释放,判断是否为全局block,就返回了否则进入下一步。
    image.png
  • _Block_copy block的创建编译时是一个栈block,只有通过 block_copy操作后才会成为一个堆block.到运行时发现捕获了外界变量,就会根据需要捕获的内存大小

image.png 申请一段内存空间,拷贝栈block中的所有内容,成为一个堆block

image.png 这里重新标记位堆block

  • Block_descriptor_1 *descriptor; image.png 这是一段内存连续的可选参数,block类型不一样,这里的数据结构也会不一样。

  • 如果类型是 Block_descriptor_2 就会有copy和dispose两个函数方法 Function image.png 如何成为Block_descriptor_2

  • 发现获取Block_descriptor_2是通过内存平移Block_descriptor_1大小获得 image.png

  • Block_descriptor_3 也是同理的, image.png

  • 案例图片介绍 image.png

cpp分析整个流程

  • 写一个简单的block在viewcontroller. image.png

  • 通过终端变成cpp文件后,开始分析这个block的运行过程 image.png

  • 创建block

image.png

  • 生成了这样的一个结构体的block image.png

  • 看desc,发现这个内存空间已经在创建block的时候赋值给他了 image.png

  • copy函数和dispose函数

image.png copy函数真实调用了_Block_object_assign函数,

  • 捕获变量的相关处理,即捕获了不同类型的成员变量,这里做了不一样的处理 image.png image.png

  • BLOCK_FIELD_IS_OBJECT:普通对象类型,就BLOCK_FIELD_IS_OBJECTarc系统函数处理后,地址指针的赋值。

  • BLOCK_FIELD_IS_BLOCK:block类型,就会做block_copy操作,把这个block对象的地址指针赋值保存。

image.png 这里是__weak,__block,修饰的变量,做_Block_byref_copy操作

  • 如果不是一个正常的对象,引用计数+1, image.png

image.png copy->forwarding = copy; src->forwarding = copy;改变原来捕获变量forwarding和我block中存放的变量的forwarding相同,所以两个是一个东西。

  • __Block_byref_objc1_0 这个被__block修饰的对象最后转变的结构体是这个类型

image.png

  • 查看cpp文件发现这个结构体是这样的 image.png
  • isa = 8字节
  • forwarding 8
  • flags 4
  • size 4
  • 两个Function 8 + 8
  • objc1

image.png

这里创建出结构体后,byref_keep = 就是__Block_byref_id_object_copy_131 这个函数

image.png 这里会再次调用block_objc_assign

image.png 就是在这里调用的,+40 个字节平移刚刚好就是 __Block_byref_objc1_0结构体中的 objc1

image.png 在递归一次这里的调用。 所以得出结论 block捕获变量会根据不同类型来进行不同的操作。 block的释放就是byref_destroy也是根据不同类型进行不同的操作释放。