OC底层原理28-Block下

502 阅读6分钟

这是我参与8月更文挑战的第12天,活动详情查看:8月更文挑战

前言

  • block底层源码
  • block编译成一个什么样的结构
  • block invoke-isa-签名-捕获-保存-释放

libclosure-79

一.block通过clang分析

1.未捕获外部变量

int main(){

    int a = 5;
    void(^block)(void) = ^{
    };
    block();
    return 0;
}

通过 xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc block.c -o block.cpp 生成.cpp文件 查看cpp文件

Xnip2021-08-25_00-02-02.jpg

block是一个__main_block_impl_0结构体

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

而我们的cpp调用的就是这个结构体中的构造函数__main_block_impl_0 该构造函数对block结构体中相关属性进行设置

 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;//方法保存 block任务保存在FuncPtr中
    Desc = desc;
  }
  1. void(*block)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
  2. block->FuncPtr(block); 调用FuncPtr方法传入block

2.捕获未使用__block修饰的外部变量

int main(){

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

Xnip2021-08-25_00-27-51.jpg 通过上面的代码发现:

  • 当捕获外部变量时,block结构体中会多一个成员变量a,并且构造函数也会多一个参数a
  • 如果没有__block修饰,则通过值拷贝的方式,对其成员变量a进行赋值
  • 在执行block任务时,从结构体中获取对应的成员变量__cself->a,进行处理
  • 我们还发现编译时isa指向的是一个栈block 按以往这个应该是个堆block 那么在运行时做了什么变为堆block

3.捕获使用__block修饰的外部变量

int main(){

    __block int a = 5;
    void(^block)(void) = ^{
        a++;
        printf("%d",a);
    };
    block();
    return 0;
}

__block修饰的外部变量又有什么区别呢,见下面编译结果:

Xnip2021-08-25_00-50-26.jpg

  • 当外部变量使用__block修饰时,会封装成一个结构体__Block_byref_a_0

  • block结构体中,多出一个属性a,属性a的类型为__Block_byref_a_0

  • a的地址会赋值到__Block_byref_a_0结构体的__forwarding属性中

二.block汇编分析得到签名copy过程

block设置断点 查看汇编

Xnip2021-08-25_23-12-24.jpg 设置objc_retainBlock系统符号断点 继续运行

Xnip2021-08-25_23-19-30.jpg

发现objc_retainBlock来自libobjc.A.dylib 也就是我们熟悉的objc库 在 libobjc.A.dylib源码里面查找objc_retainBlock

Xnip2021-08-25_23-21-14.jpg 我们查看一下 _Block_copy 发现不属于libobjc.A.dylib

Xnip2021-08-25_23-22-40.jpg 可以发现_Block_copylibsystem_blocks.dylib库中

通过跟踪汇编的方式,跟踪其内存变化过程。当程序运行到objc_retainBlock时,通过读取寄存器,分析block的数据状态变化。见下图:

Xnip2021-08-25_23-35-40.jpg

Xnip2021-08-25_23-34-50.jpg

调用objc_retainBlock方法时,此时依然是一个栈block。继续跟踪汇编,运行至_Block_copy,很显然block通过该方法完成了内存的变化,如何验证呢?汇编流程很长,在retq的地方设置断点,也就是在方法return的地方设置断点,查看其最终的处理结果,见下图:

Xnip2021-08-25_23-40-14.jpg

Xnip2021-08-25_23-40-25.jpg

同样通过读取寄存器x0,获取block此时为__NSMallocBlock__,并且地址发生了改变,从栈区拷贝到了堆区。

通过libclosure-79 搜索 _Block_copy 如下图:

Xnip2021-08-25_23-47-48.jpg

至此,可以得出结论,捕获了外部变量的block,编译时是栈block,在运行时通过_Block_copy方法会copy到堆区,变成堆block

**po [NSMethodSignature signatureWithObjCTypes:"v16@?0"]**

<NSMethodSignature: 0x60000043faa0>

    number of arguments = 1

    frame size = 224

    is special struct return? NO

    return value: -------- -------- -------- --------

        type encoding (v) 'v'

        flags {}

        modifiers {}

        frame {offset = 0, offset adjust = 0, size = 0, size adjust = 0}

        memory {offset = 0, size = 0}

    argument 0: -------- -------- -------- --------

        type encoding (@) '@?'

        flags {isObject, isBlock}

        modifiers {}

        frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}

        memory {offset = 0, size = 8}

        block签名'@?'

三.blocklayout的结构

Block_layout中包括一些属性:

  • isa isa指向确定block类型
  • flags 标识码
  • reserved 保留字段
  • invoke 函数,也就是FuncPtr
  • descriptor 相关附加信息 Xnip2021-08-26_00-42-09.jpg blockBlock_descriptor_1相关属性是必然存在,其中reserved为保留字段,sizeblock的大小;但是Block_descriptor_3是可选的参数。而这里就通过flag字段来判断block是否存在Block_descriptor_3的相关属性。Block_descriptorget方法可以发现,通过地址平移的方式获取对应的值,并且在获取Block_descriptor_3时会判断Block_descriptor_2是否存在,如果不存在,就不需要添加Block_descriptor_2的地址空间。见下图:

Xnip2021-08-26_00-42-28.jpg 我们可以通过lldb进行相关的验证: Xnip2021-08-26_00-41-30.jpg 签名在Block_descriptor_3signature属性中。

四.block的捕获变量生命周期

block是如何捕获外部变量的呢,block三重拷贝过程是怎样的?回到cpp文件!查看__ViewController__viewDidLoad_block_desc_0结构体的定义。见下图:

Xnip2021-08-26_01-24-01.jpg

该结构体即对应源码中的__ViewController__viewDidLoad_block_desc_0信息。其中reservedsize对应Block_descriptor_1的两个属性;另外,void (*copy)void (*dispose)对应Block_descriptor_2的两个方法;在copy方法的实现中,会调用_Block_object_assign,此过程即为外部变量的捕获和释放过程。

在源码中全局搜索_Block_object_assign,得到以下注释信息:

Xnip2021-08-26_01-29-34.jpg

_Block_object_assign_Block_object_disposeflags参数设置为:

  • BLOCK_FIELD_IS_OBJECT (3),捕获Objective-C Object的情况
  • BLOCK_FIELD_IS_BLOCK (7),捕获另一个block的情况
  • BLOCK_FIELD_IS_BYREF (8),捕获__block变量的情况 其枚举定义,见下图:

Xnip2021-08-26_01-32-05.jpg

_Block_object_assign源码分析

//
// When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point
// to do the assignment.
//
void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      case BLOCK_FIELD_IS_OBJECT:
        /*******
        id object = ...;
        [^{ object; } copy];
        ********/
        // _Block_retain_object_default = fn (arc)
        //如果持有变量是`BLOCK_FIELD_IS_OBJECT`类型,即没有`__block`修饰,指针指向该对象,将对该对象进行持有,引用计数加`1`
        _Block_retain_object(object);
        *dest = object;
        break;

      case BLOCK_FIELD_IS_BLOCK:
        /*******
        void (^object)(void) = ...;
        [^{ object; } copy];
        ********/
        //如果是`BLOCK_FIELD_IS_BLOCK`类型,捕获一个`block`,则进行`_Block_copy`操作
        *dest = _Block_copy(object);
        break;
    
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        /*******
         // copy the onstack __block container to the heap
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __block ... x;
         __weak __block ... x;
         [^{ x; } copy];
         ********/

        //如果是BLOCK_FIELD_IS_BYREF,即有`__block`修饰,则会调用`_Block_byref_copy`
        *dest = _Block_byref_copy(object);
        break;
        
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        /*******
         // copy the actual field held in the __block container
         // Note this is MRC unretained __block only. 
         // ARC retained __block is handled by the copy helper directly.
         __block id object;
         __block void (^object)(void);
         [^{ object; } copy];
         ********/

        *dest = object;
        break;

      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK  | BLOCK_FIELD_IS_WEAK:
        /*******
         // copy the actual field held in the __block container
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __weak __block id object;
         __weak __block void (^object)(void);
         [^{ object; } copy];
         ********/

        *dest = object;
        break;

      default:
        break;
    }
}

_Block_byref_copy实现源码

static struct Block_byref *_Block_byref_copy(const void *arg) {
    struct Block_byref *src = (struct Block_byref *)arg;

    // __block 内存是一样 同一个家伙
    //
    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // src points to stack
        struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
        copy->isa = NULL;
        // byref value 4 is logical refcount of 2: one for caller, one for stack
        copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
        copy->forwarding = copy; // patch heap copy to point to itself
        src->forwarding = copy;  // patch stack to point to heap copy
        copy->size = src->size;

        if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
            // Trust copy helper to copy everything of interest
            // If more than one field shows up in a byref block this is wrong XXX
            struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
            struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
            copy2->byref_keep = src2->byref_keep;
            copy2->byref_destroy = src2->byref_destroy;

            if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
                struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
                struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
                copy3->layout = src3->layout;
            }

            // 捕获到了外界的变量 - 内存处理 - 生命周期的保存
            (*src2->byref_keep)(copy, src);
        }
        else {
            // Bitwise copy.
            // This copy includes Block_byref_3, if any.
            memmove(copy+1, src+1, src->size - sizeof(*src));
        }
    }
    // already copied to heap
    else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
        latching_incr_int(&src->forwarding->flags);
    }
    
    return src->forwarding;
}

  • 将外部对象封装成结构体Block_byref *src
  • 如果是BLOCK_FIELD_IS_BYREF,则会调用malloc,生成一个Block_byref *copy
  • 设置forwarding,保证block内部和外部都指向同一个对象
copy->forwarding = copy; 
src->forwarding = copy;

Block_byrefkeep函数和destroy处理,并进行byref_keep函数的调用

Xnip2021-08-26_01-37-52.jpg

Block_byref的设计思路和Block_layoutdescriptor流程类似,通过byref->flag标识码判断对应的属性,以此来判断Block_byref_2是否存在,Block_byref定义见下图:

Xnip2021-08-26_01-38-55.jpg

如果用__block修饰了外部变量,编译生成的cpp文件中,Block_byref结构体中就会默认生成两个方法,即对应Block_byref_2keep方法和destory方法,见下图:

Xnip2021-08-26_01-40-07.jpg

cpp文件中搜索这两个函数的实现,见下图:

Xnip2021-08-26_01-40-28.jpg

此过程会再次调用_Block_object_assign函数,对Block_byref结构体中的对象进行BLOCK_FIELD_IS_OBJECT流程处理。

至此block的三重拷贝流程如下:

  1. block的拷贝,即将栈区block,拷贝至堆区
  2. __block修饰的对象,对应的Block_byref结构体的拷贝
  3. Block_byref修饰的对象,调用_Block_object_assign函数进行修饰处理