iOS Block探究二

363 阅读3分钟

Block clang分析

通过命令clang -rewrite-objc main.m -o main.cpp将下面的代码编译成cpp

截屏2021-08-31 上午10.18.31.png

block底层结构

编译成cpp文件之后,找到对应的main方法,然后找到对应block代码如下:

int a = 18;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

这个代码看的很懵逼,我们吧对应的类型去掉简化一下:

截屏2021-08-31 上午10.32.05.png

  • __main_block_impl_0方法实现
  • block->FuncPtr(block)调用 block底层是一个结构体__main_block_impl_0,然后继承__block_impl__block_impl定义如下
struct __block_impl {
  void *isa; //指向区域
  int Flags; //标记
  int Reserved;
  void *FuncPtr; //调用方法
};

__main_block_impl_0(实现)

截屏2021-08-31 上午10.46.04.png

  • 通过__main_block_impl_0初始化赋值block
  • __main_block_func_0传入了block本身,然后进行block代码的实现。

__block修饰外部变量

如果想要在block内部修改外部变量,我们需要用__block修饰,那么__block究竟做了啥。用__block修饰外部变量,然后重新clang一下。

截屏2021-08-31 上午10.57.48.png 发现变量用__Block_byref_a_0结构体包装了一下,然后传入给 block。__Block_byref_a_0:内部定义

截屏2021-08-31 上午11.10.23.png 实际上forwarding传入的就是外部变量的地址。

截屏2021-08-31 上午11.13.12.png 然后我们在调用函数里面访问的也是a的地址,所以我们在block改变的变量,实际上就是外部变量。

block copy

在block声明的地方下断点,然后通过汇编和符号断点objc_retainBlock->_Block_copy。_Block_copy在libsystem_blocks.dylib中,由于这个库没有开源,可以用libclosure代替。

截屏2021-08-31 下午6.09.58.png

  • block 的本质是一个Block_layout
  • 如果是全局block,直接返回
  • 如果是栈block,捕获了外部变量 在_Block_copy打断点,通过register read x0,在最开始的时候传入时候block是什么类型 截屏2021-09-01 下午2.33.23.png 然后在return的时候打印返回的类型:

截屏2021-09-01 下午2.39.14.png 然后分别打印x0寄存器 截屏2021-09-01 下午2.39.44.png 总结:在编译的时候是stack block,如果捕获了外部变量就会从拷贝到堆区,变成堆block.

block_layout结构

struct Block_layout {

    void *isa;  //指向确定block类型
    volatile int32_t flags; // 标识码,包含是否有签名、正在析构
    int32_t reserved; //保留字段
    BlockInvokeFunction invoke;// 函数,也就是FuncPtr
    struct Block_descriptor_1 *descriptor; //附加信息
    // imported variables
};

#define BLOCK_DESCRIPTOR_1 1

**struct** Block_descriptor_1 {

    uintptr_t reserved;

    uintptr_t size;

};

\


#define BLOCK_DESCRIPTOR_2 1

struct Block_descriptor_2 {

    // requires BLOCK_HAS_COPY_DISPOSE

    BlockCopyFunction copy;

    BlockDisposeFunction dispose;

};

\
#define BLOCK_DESCRIPTOR_3 1

struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
 const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT

};

其中Block_descriptor_2Block_descriptor_3都是可选字段。全局搜索get方法:

截屏2021-09-01 下午5.15.24.png 这里Block_descriptor_2Block_descriptor_3是在Block_descriptor_1的基础上做了内存平移处理。

block 捕获变量生命周期

xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc ViewController.m -o ViewController.cpp 通过上面的命令堆block进行clang分析,这里捕获objdect类型,没有用__block修饰。

截屏2021-09-01 下午6.08.24.png

  • 通过描述字段__ViewController__viewDidLoad_block_desc_0,这个实际上就是对应源码里面Block_descriptor_1Block_descriptor_2Block_descriptor_3
  • 看copy方法__ViewController__viewDidLoad_block_copy_0,发现最终调用了_Block_object_assign

_Block_object_assign分析

源码对不同的类型做了不同的处理

截屏2021-09-01 下午6.20.27.png

  • BLOCK_FIELD_IS_OBJECT ,如果是对象类型,直接赋值
*dest = object;
  • BLOCK_FIELD_IS_BLOCK,如果捕获的是block类型,则调用_Block_copy拷贝
  • BLOCK_FIELD_IS_BYREF,如果使用__block修饰,则会调用_Block_byref_copy
  • 其他情况都是直接赋值

_Block_byref_copy分析

object对象传入会包装一成一个Block_byref对象,原对像。

struct Block_byref *src = (struct Block_byref *)arg;

重新开辟空间,生成一个dst对象

struct Block_byref *copy = (struct Block_byref *)malloc(src->size);

设置forwarding,保证block内部和外部都指向同一个对象

copy->forwarding = copy; // 
src->forwarding = copy;  //

byref_keep 和 byref_keep处理

copy2->byref_keep = src2->byref_keep;
copy2->byref_destroy = src2->byref_destroy;

// 捕获到了外界的变量 - 内存处理 - 生命周期的保存
 (*src2->byref_keep)(copy, src);

但是这个保存做了啥,源码里面搜索不到,我们用__block修饰,然后再生成.cpp文件,打开

  • 包装的Block_byref会有_Block_byref_id_object_copy__Block_byref_id_object_dispose

截屏2021-09-01 下午6.49.50.png 发现会再次调用_Block_object_assign拷贝。 调用:_Block_object_dispose释放

总结:所以在使用__block的修饰的变量的时候,会有二次拷贝。加上block从栈拷贝到堆就是3次.