iOS底层-深入浅出Block分析

397 阅读16分钟

前言

在开发中Block的使用是必不可少的,它可以作为参数或者函数的返回值。但使用Block过程中,还是多少会有些问题,下面我们对它进行一个全面的分析

常见的Block

  • 常见的Block有三种:

      1. GlobalBlock
      • 位于全局区,在Block内部不使用外部局部变量,或者只使用静态变量或者全局变量
      1. MallocBlock
      • 位于堆区,在Block内部使用局部变量或者OC属性,并且赋值给强引用或者Copy修饰的变量
      1. StackBlock
      • block位于栈区,与MallocBlock一样可以在内部使用局部变量或者OC属性,但是不能赋值给强引用或者Copy修饰的变量。
  • 3种BlockARC中的代码体现:

    截屏2021-08-24 07.00.47.png

可能存在的误区

  • 手动CopyBlock就变成了MallocBlock吗?
    • 不一定,如果没有使用局部变量或者OC属性,即使copy还是原来的类型
    截屏2021-08-25 10.59.06.png

Block对引用计数的影响

  • 先来看一个题目
    - (void)blockRetainCountTest {
        NSObject *objc = [NSObject new]; // 1
        NSLog(@"print1 retainCount %ld", CFGetRetainCount((__bridge CFTypeRef)(objc)));
    
        void (^strongBlock)(void) = ^{ // MallocBlock
            NSLog(@"print2 retainCount %ld", CFGetRetainCount((__bridge CFTypeRef)(objc)));
        };
        strongBlock();
    
        void (^__weak weakBlock)(void) = ^{ // StackBlock
            NSLog(@"print3 retainCount %ld", CFGetRetainCount((__bridge CFTypeRef)(objc)));
        };
        weakBlock();
    
        void (^ mallocBlock)(void) = [weakBlock copy]; // MallocBlock
        mallocBlock();
    }
    
    • 这个几个打印分别打印几呢?print1毋庸置疑肯定是1,那么第二个呢?先来看下输出结果:

      截屏2021-08-24 21.57.53.png

    • 打印结果为1,3,4,5,这里可能就会有疑问了,第二个怎么是3?分析如下:

      • 第二个是MallocBlock,它对objc进行了持有,所以引用计数+1,但是在底层堆Block会拷贝一份,所以引用技术再+1就变成了3
      • 第三个是StactBlock,它只对objc进行了持有,所以引用计数+1之后变成4
      • 第四个Block是对栈Block进行了copy变成了MallocBlock,引用计数再+1变成5
      • 第三和第四种结合起来就是第二种Block的情况

Block堆栈释放差异

  • 一、来看看Block再对栈中的案例:

    - (void)blockTest {
        int a = 20;
        void(^__weak weakBlock)(void) = nil;
        {
            void(^__weak strongBlock)(void) = ^{
                NSLog(@"---%d", a);
            };
            weakBlock = strongBlock;
            NSLog(@"1 - %@ - %@",weakBlock,strongBlock);
        }
        weakBlock();
    }
    
    • 这个打印是怎样呢?

    截屏2021-08-24 23.20.46.png

    • 结果正常执行。这里可能会产生的误区:

        1. {}是作用域,不是block块
        1. weakBlockStackBlock,它的作用域与{}无关,与blockTest函数有关,所以出了{}后它的内存还在不会释放,所以可以正常调用
    • 如果a换成对象类型,又是怎样呢?

      截屏2021-08-25 19.43.29.png

      • 结果a无法打印,为什么?
      • 因为在strongBlock中捕获了a指针,捕获对象会copy一份到堆,所以出作用域{}后,捕获的这个a内存会被释放,所以会打印null
  • 二、将题目修改下把strongBlock__weak去掉,然后再运行:

    截屏2021-08-25 10.14.07.png

    • 此时产生崩溃why

      • 由于此时的strongBlockMallocBlock,它在出{}后释放了,所以外面weakBlock调用时,实质是空地址调用,于是就产生崩溃

      截屏2021-08-25 10.19.17.png

  • 三、两个block都是MallocBlock

    截屏2021-08-25 10.22.48.png

    • 这又可以输出,不是出作用域释放了么?
      • 此时weakBlockstrongBlock都是MallocBlock,赋值后weakBlockstrongBlock进行了 强持有,所以出{}作用域后strongBlock并没有释放,所以可以调用成功

循环引用

产生循环引用的原因

  • Block常见的问题就是循环引用,什么是循环引用呢?,首先来看下对象的持有与释放流程截屏2021-08-25 14.17.50.png

    • 当对象B被持有时引用计数会+1,当A需要释放时,A会发送release信号给B,如果retainCount0,则B发送dealloc信号给A,然后A释放
    • 循环引用的过程也比较好理解,就是A持有B,但B中也有AA需要释放时就会发送releaseB,等待B发送dealloc消息,但B因持有A所以需要A释放后才会发送,所以就形成了相互等待,如下图:

    截屏2021-08-25 14.17.59.png

案例分析

  • 经典案例:

    @property (nonatomic, copy) dispatch_block_t block;
    @property (nonatomic, copy) NSString *name;
    
    - (void)retainCycleDemo1 {
        self.name = @"wushuang";
        self.block = ^{
            NSLog(@"name is %@", self.name);
        };
        self.block();
    }
    
    - (void)dealloc
    {
        NSLog(@" 释放啦~~~ 🎉🎉🎉 ");
    }
    
    • 代码中self -> block -> self,导致循环引用,进而导致VC无法释放

    截屏2021-08-25 14.46.21.png

    那么这个问题怎么解决呢?我们都知道用weakSelf来解决,但问题真能解决吗?

    截屏2021-08-25 14.56.41.png

    哦豁,确实可以释放了,但任务还没执行就销毁了,不是我们想要的效果啊,此时就需要用weak-strong搭配使用了,也就是强弱共舞

解决方案

    1. 强弱共舞

    截屏2021-08-25 15.03.34.png

    • strongSelf是一个局部变量,也就是self在块任务这个区间被强引用,当任务执行完也就不存在了。当需要dealloc时,如果block没有执行完,但由于此强引用self,所以不会第一时间释放,等任务结束没有了strongSelf就会继续执行dealloc
    1. 手动断联
    • 手动断开联系的思想就是:使用一个临时变量,然后使用完自动置为nil,这样也能解决循环引用问题

    截屏2021-08-25 15.15.32.png

    • __block修饰的原因是,在block块中能够被修改
    • 此时的vcself都是指向同一块内存,但是本身是两个不同指针
    • 如果不置为nil,在dealloc时,因为block持有vcvc指向ViewControler的内存导致无法释放
    1. 自动断联(局部变量)
    • block改成有参数无反@property (nonatomic, copy) void(^block)(ViewController *),然后做一些相应修改再运行

    截屏2021-08-25 15.24.06.png

    • 此时self只是个参数,在使用完就自动释放了~

面试题

  • 题目1:

    static ViewController *staticSelf;
    
    - (void)testWeakStaticBlock {
        __weak typeof(self)weakSelf = self;
        staticSelf = weakSelf;
    }
    
    • 这个会产强引用,staticSelf在是一个全局静态变量,weakSelf指向的是ViewController,也就是一直访问ViewController导致无法释放
  • 题目2:

    @property (nonatomic, copy) dispatch_block_t wushuangBlock;
    @property (nonatomic, copy) dispatch_block_t dianJiBlock;
    
    - (void)testWeakStrongBlock {
        __weak typeof(self) weakSelf = self;
        self.wushuangBlock = ^{
            __strong typeof(self) strongSelf = weakSelf;
            weakSelf.dianJiBlock = ^{
                NSLog(@"%@", strongSelf);
            };
           weakSelf.dianJiBlock();
        };
       self.wushuangBlock();
    }
    
    • 这个题会导致不会释放,正常的逻辑是在wushuangBlockstrongSelf会在回调完成销毁。 但它被dianJiBlock所持有,导致strongSelf引用计数再+1也就是2,当出作用域wushuangBlock时会进行release操作引用计数进行减1变为1,所以无法释放导致不会释放

Block底层原理

  • 此时对Block有这样几个疑问:
      1. blockcopy到底做了什么,它的原理是怎样的?
      1. block捕获的变量去哪了?
      1. 怎么知道block捕获了变量?捕获的变量又是怎么释放的?
      1. block在底层编译成一个什么样的结构?

带着这些问题,我们去底层一探究竟

捕获分析

  • BlockC++源码结构

    • 先看BlockC++中是以怎样的形态存在
    int wushuangAge = 18;
    void (^wushuangBlock)(void) = ^{
        NSLog(@" wushuang %@", wushuangAge);
    };
    wushuangBlock();
    
    • clang编译成C++后:
    int wushuangAge = 18;
    void (*wushuangBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, wushuangAge));
    ((void (*)(__block_impl *))((__block_impl *)wushuangBlock)->FuncPtr)((__block_impl *)wushuangBlock);
    
    • C++中有一堆的类型强转,去掉这些后就变的比较清晰了
    int wushuangAge = 18;
    void (*wushuangBlock)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, wushuangAge);
    wushuangBlock->FuncPtr(wushuangBlock);
    
    • 此时block块被包装成__main_block_impl_0函数,它的结构如下
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int wushuangAge;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _wushuangAge, int flags=0) : wushuangAge(_wushuangAge) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        int wushuangAge = __cself->wushuangAge; // bound by copy
    
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_7t_93mn_rbs0p1d7bkzy4z_f0s40000gn_T_main_55fd88_mi_0, wushuangAge);
    }
    
    • __main_block_impl_0是一个结构体,它有自己的构造函数,函数调用时也就是调用FuncPtr,由于参数fp__main_block_func_0,所以函数的调用实质是调用__main_block_func_0方法,里面的wushuangAge和结构体里的wushuangAge值相同,但各自内存不同,可以理解为此处是值copy

    • 在结构体的成员变量中有个wushuangAge,那么这个是不是捕获的变量有关呢?接下来去掉NSLogwushuangAge的打印,再次编译得到:

    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;
      }
    };
    
    • 没有捕获变量时,成员变量只有implDesc两个,但isa类型依然是_NSConcreteStackBlock

    • 再捕获对象类型查看下C++源码,然后对比捕获int类型时的__main_block_func_0的差异

    • OC代码如下

    NSObject *dianjiObj = [NSObject new];
    void (^wushuangBlock)(void) = ^{
        NSLog(@" wushuang %@", dianjiObj);
    };
    wushuangBlock();
    
    • 编译成C++源码: 截屏2021-08-26 08.43.36.png
    • 此处__main_block_func_0中的赋值是一个指针,所以此处的dianjiObj和成员dianjiObj都是指向同一片内存是一样的,另外这里的isa也是_NSConcreteStackBlock类型
  • __block分析

    • int变量前面添加__block
    __block int wushuangAge = 18;
    void (^ wushuangBlock)(void) = ^{
        wushuangAge++;
        NSLog(@" wushuang: %d", wushuangAge);
    };
    wushuangBlock();
    
    • 然后查看底层C++源码:
    __attribute__((__blocks__(byref))) __Block_byref_wushuangAge_0 wushuangAge = {(void*)0,(__Block_byref_wushuangAge_0 *)&wushuangAge, 0, sizeof(__Block_byref_wushuangAge_0), 18};
    
    void (* wushuangBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_wushuangAge_0 *)&wushuangAge, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)wushuangBlock)->FuncPtr)((__block_impl *)wushuangBlock);
    
    // 将结构__Block_byref_wushuangAge_0 格式化下:
    __attribute__((__blocks__(byref))) __Block_byref_wushuangAge_0 wushuangAge = {
         (void*)0,
         (__Block_byref_wushuangAge_0 *)&wushuangAge,
         0,
         sizeof(__Block_byref_wushuangAge_0),
         18
    };
    
    // 结构体
    struct __Block_byref_wushuangAge_0 {
        void *__isa;
        __Block_byref_wushuangAge_0 *__forwarding;
        int __flags;
        int __size;
        int wushuangAge;
    };
    
    • 此时出现了个__Block_byref_wushuangAge_0结构,它是个结构体,此时的__forwarding指针指向wushuangAge,函数调用处如下:

    截屏2021-08-26 09.20.54.png

    • 此时的赋值得到的wushuangAge指针和外面的都指向同一片内存,也就是指针拷贝,同时isa也是_NSConcreteStackBlock类型。
    • 此时对以往的观点恍然大悟:__block修饰变量的值能在block块中改变是因为它是指针拷贝;不加__block修饰,变量的改变实质是block块中临时变量在改变值,和外界的变量没有关系

对比这几种情况,于是得到以下结论:

结论:
1. block在底层是一个 结构体
2. 当捕获变量为int型时,是值copy
3. 当捕获对象类型时是指针拷贝
4. 使用__block修饰的变量在底层是指针拷贝
5. block的初始类型都是StackBlock

  • 现在我们知道Block初始类型都是StackBlock,那么在运行时是怎么变成MallocBlock的,__block做了什么导致生产了结构体__Block_byref_xxx_0,还有些上面提出的疑问还没解决,接下来我们去Block底层找答案

  • 汇编分析

    • 先定义一个普通block
    void (^block)(void) = ^{
        NSLog(@" wushuang ");
    };
    block();
    
    • 然后断点进去看汇编,出现一个objc_retainBlock函数

    截屏2021-08-26 11.23.30.png

    • 继续往下面走,会走到libobjc源码的objc_retainBlock函数

    截屏2021-08-26 11.25.38.png

    • 于是进入objc4-818.2 查看是否有相关代码:
    id objc_retainBlock(id x) {
        return (id)_Block_copy(x);
    }
    
    • 源码中只返回一个_Block_copy方法,并没有次方法的实现,只能跟着汇编继续往下跟,于是走到了libsystem_blocks.dylib

    截屏2021-08-26 11.33.17.png

    • 不过libsystem_blocks源码并没有开源,但在 libclosure-79 找到了_Block_copy的实现
  • 把普通的Block改成MallocBlock类型,然后真机运行

    NSObject *obj = [NSObject new];
    void (^block)(void) = ^{
        NSLog(@" wushuang %@", obj);
    };
    block();
    
    • 然后在进入汇编,bl objc_retainBlock时,读x0寄存器:

    截屏2021-08-26 16.29.01.png

    • 此时的Block类型已经变成了StackBlock,然后继续走汇编进入_Block_copy,走到ret,再次读x0寄存器:

    截屏2021-08-26 16.30.58.png

    • 可以发现经过_Block_copy后,类型从StackBlock变成_Block_copy,那这个过程是怎样的,就需要去libclosure-79源码进行研究

变成MallocBlock

  • libclosure-79中找到_Block_copy,源码实现代码如下:

    截屏2021-08-26 15.59.47.png

  • _Block_copy主要进行了以下几个步骤的操作:

      1. 根据参数ary强转成Block_layout类型的对象aBlockBlock_layout结构如下:
      struct Block_layout {
          void * __ptrauth_objc_isa_pointer isa; // 指向
          volatile int32_t flags; // contains ref count //标识符
          int32_t reserved;
          BlockInvokeFunction invoke; //调用函数
          struct Block_descriptor_1 *descriptor;
          // imported variables
      };
      
      1. 根据标识符判断类型,如果是释放类型则做一些相关处理;
      1. 如果是GlobalBlock类型则直接返回对象
      1. 如果是Malloc类型则会进行下面步骤创建一份:计算大小 -> 根据内存创建新对象 -> 拷贝一份新对象 -> 进行相关参数赋值 -> isa类型变成MallocBlock -> 返回新对象

方法签名

  • 在汇编打印时,能够发现有signature签名,invoke回调函数等,这个signature就是block的签名
    • v8@?0中的v代表返回值,8代表字节大小,@代表id类型,?未知,0是第0个位置,拿到签名值可以使用[NSMethodSignature signatureWithObjCTypes:"v8@?0"]查看:
    截屏2021-08-26 16.36.15.png
    • 签名中可以看到参数的个数,Block返回值类型,签名等信息,此处又可以得出结论

结论Block的方法签名是:@?

  • 此时又有问题了,在读x0寄存器打印Block时,出现了copydispose参数,但在它的成员descriptor中没有这两个成员

    struct Block_descriptor_1 {
        uintptr_t reserved; // 8
        uintptr_t size; // 8
    };
    
    #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_2中有这两个参数,然后Block_descriptor_3中也有相关的参数signature参数,但是Block结构虽然能打印signaturecopydispose,但结构体中并没有,这是怎么回事?其实这个是内存的连续可选

参数可选

  • Block_layout结构体下面有注释imported variables(添加更多变量),那么是通过怎样来添加更多变量的呢,其实是通过BLOCK_DESCRIPTOR_2或BLOCK_DESCRIPTOR_3标识符来操作的

  • 搜索Block_descriptor_2发现并没有它是怎么生成的,但我们可以换个思路,可以找他是怎么获取的,这也就是逆向思维,于是就找到了相关函数:

    截屏2021-08-26 18.33.24.png

      1. 这里首先根据_Block_get_descriptor函数获取Block_descriptor_1首地址: 截屏2021-08-26 18.42.18.png
      1. 然后通过内存平移 Block_descriptor_1个内存大小(16字节)来获得Block_descriptor_2
    • Block_descriptor_3的获取也一点差异,也是先获取首地址,然后平移Block_descriptor_1内存大小(16字节)
      • 当是BLOCK_HAS_COPY_DISPOSE类型,就会再加上Block_descriptor_2内存大小(16字节),从而得到Block_descriptor_3
      • 当不是这个类型,就会直接返回,也就是Block_descriptor_2的位置
  • 此时我们可以通过x/8gx来打印内存的分布:

    截屏2021-08-26 18.51.42.png

    • Block_layout结构的第一个参数是isa占用8字节,所以在第一段内存,此时可以打印出来是MallocBlock

    • 第二个参数flags和第三个参数reserved都是4字节,由于一段内存占8字节,所以第二段内存足够存放两个参数

    • 第三段内存是invoke,和上面打印的一致

    • 所以第四段肯定就是Block_descriptor_1的内存首地址,此时可以x/8gx来打印这个地址后面内存的分布:

      截屏2021-08-26 19.01.06.png

      • 第一和第二段分别代表Block_descriptor_1reservedsize参数
      • 第三段和第四段内存地址和寄存器打印的copydispose内存一致,也就是Block_descriptor_2中的内容
      • 第五段内存通过打印发现是v8@?0也就是签名,这里是Block_descriptor_3中的内容

三层copy

  • 现在还有捕获变量的问题没有解决,底层到底是怎么捕获的,捕获的变量什么时候释放

  • block添加__block修饰:

    __block NSObject *dianjiObj = [NSObject new];
    void (^wushuangBlock)(void) = ^{
        NSLog(@" wushuang %@", dianjiObj);
    };
    wushuangBlock();
    
    • 将代码编译成C++截屏2021-08-27 08.45.34.png

    • 再观察发现有两个参数__Block_byref_id_object_copy_131__Block_byref_id_object_dispose_131,他们的实现都差不多

    截屏2021-08-27 08.54.00.png

    • 他们的实现分别是_Block_object_assign_Block_object_dispose,参数里都有内存平移40的操作,那么这个平移后得到的是什么呢?那要从本身的结构去分析了

    截屏2021-08-27 08.56.11.png

    • 结构中内存平移40后刚好是dianjiObj,所以可以断定上面传入的都是dianjiObj
  • 再看wushuangBlock结构体的第二个参数__main_block_desc_0_DATA,他的结构是Block_descriptor_1加上Block_descriptor_2:

    截屏2021-08-27 09.03.48.png

    • 根据构造函数可以知道copydispose对应的就是__main_block_copy_0__main_block_dispose_0函数,那这个两个函数又做了什么呢?

    截屏2021-08-27 09.49.23.png

    • 搜索发现最终执行的分别是_Block_object_assign_Block_object_dispose,只不过此时第三个参数都是8,于是我们可以断点这两个函数肯定很重要,下面对这两个方法进行分析

_Block_object_assign

  • libclosure-79源码中搜索该方法,有如下注释 截屏2021-08-27 10.10.22.png

    • 注释中说,当Block被复制到堆中时,Block可以引用四种不同方式来提供帮助。
        1. 基于c++堆栈的对象
        1. 对Objective-C对象的引用
        1. 其他模块
        1. __block变量
    • 在这些情况下,编译器合成辅助函数用于Block_copyBlock_release,称为copydispose辅助函数。对于基于c++堆栈的对象,复制助手发出对c++ const复制构造函数的调用,以及对运行时支持函数_Block_object_assign的其余调用。dispose helpercase 1(第一种情况)调用c++析构函数,对其余的调用_Block_object_dispose
    • _Block_object_assign_Block_object_disposeflags参数设置为:
      • BLOCK_FIELD_IS_OBJECT(3),捕获Objective-C对象,
      • BLOCK_FIELD_IS_BLOCK(7),捕获另一个Block情况
      • BLOCK_FIELD_IS_BYREF(8),捕获__block变量的情况。
    • 如果__block变量被标记为weak,编译器也会将其标记为BLOCK_FIELD_IS_WEAK (16)
    • 因此Block copy/dispose帮助程序应该只生成3、7、8和24这四个标志值。
    • __block变量是c++对象、Objective-C对象或另一个Block时,编译器也会生成copy/dispose helper函数。与Block复制助手类似,__block复制助手(以前也称__blockbyref复制助手)将执行c++复制构造函数 (而不是const构造函数!),而dispose助手将执行析构函数。类似地,helper将调用两个具有相同对象和block值的支持函数,并提供额外的BLOCK_BYREF_CALLER(128)位信息
  • 从注释中可以发现构造函数会走_Block_object_assign方法,而析构时会走_Block_object_dispose方法

  • _Block_object_assign代码如下:

    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:
    
            _Block_retain_object(object);
            *dest = object;
            break;
            
          case BLOCK_FIELD_IS_BLOCK:
          
            *dest = _Block_copy(object);
            break;
            
          case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
          case BLOCK_FIELD_IS_BYREF:
    
            *dest = _Block_byref_copy(object);
            break;
          
          case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
          case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
    
            *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:
    
            *dest = object;
            break;
    
          default:
            break;
        }
    }
    
  • 第一次进来flag值为8对应的就是BLOCK_FIELD_IS_BYREF,所以会执行_Block_byref_copy函数

    截屏2021-08-27 12.35.32.png

    • src不在堆中,会根据size进行malloc一份,也就是copy到堆,然后对copy->forwarding指向自己,src->forwarding也指向堆区的copy对象,保证指向是一样的,然后进行相关的参数赋值,最后进行(*src2->byref_keep)(copy, src)调用,byref_keep是什么呢?
    struct Block_byref_2 {
        // requires BLOCK_BYREF_HAS_COPY_DISPOSE
        BlockByrefKeepFunction byref_keep; 
        BlockByrefDestroyFunction byref_destroy; 
    };
    
    • 根据注释得到byref_keep实质就是C++源码中的__Block_byref_id_object_copy_131,也就是继续调用_Block_object_assign函数,且flag131(128 + 3)
    • 所以此时会走进_Block_retain_object进行引用计数+1,而持有的对象通过前面分析的就是dianjiObj,不过此时_Block_retain_object的实现并没有做任何操作,但它可以进行赋值,这一步交给了ARC去进行相关的处理。
    • 所以byref_keep方法的调用,实质就是对dianjiObj进行copy,这一步主要是对象生命周期进行保存,至此三次copy结束

三层copy总结:
1. 如果是__block修饰的变量会通过_Block_copy方法将其从栈区拷贝到堆区
2. 第二次copy是捕获__Block_byref
3.第三次是__Block_byrefobject对象进行捕获

销毁

  • 现在还有个释放问题,也就是销毁问题,它主要是执行_Block_object_dispose函数,它和上面copy有些类似

    void _Block_object_dispose(const void *object, const int flags) {
        switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
          case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
          case BLOCK_FIELD_IS_BYREF:
            // get rid of the __block data structure held in a Block
            _Block_byref_release(object);
            break;
          case BLOCK_FIELD_IS_BLOCK:
            _Block_release(object);
            break;
          case BLOCK_FIELD_IS_OBJECT:
            _Block_release_object(object);
            break;
          case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
          case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
          case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
          case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK  | BLOCK_FIELD_IS_WEAK:
            break;
           default:
            break;
        }
    }
    
    • 第一次会传入flag8,会进入_Block_byref_release方法
    static void _Block_byref_release(const void *arg) {
        struct Block_byref *byref = (struct Block_byref *)arg;
    
        // dereference the forwarding pointer since the compiler isn't doing this anymore (ever?)
        byref = byref->forwarding; // 获取copy的`Block_byref`
      
        if (byref->flags & BLOCK_BYREF_NEEDS_FREE) { //释放类型
            int32_t refcount = byref->flags & BLOCK_REFCOUNT_MASK;
            os_assert(refcount); // 引用计数进行清空处理
            if (latching_decr_int_should_deallocate(&byref->flags)) {
                if (byref->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
                    struct Block_byref_2 *byref2 = (struct Block_byref_2 *)(byref+1);
                    (*byref2->byref_destroy)(byref); // 调用__Block_byref_id_object_dispose_131方法
                }
                free(byref); // 释放byref
            }
        }
    }
    
    • 主要是先拿到Block_byrefforwarding,然后根据类型进行释放处理
      • 这里也和上面copy类似会进行byref_destroy调用,也就是调用__Block_byref_id_object_dispose_131方法且flag=131,然后根据flag会走到_Block_object_dispose_Block_release方法:

      截屏2021-08-27 13.07.01.png
      • _Block_release方法主要是对Block_layout进行相关的处理和释放
  • _Block_object_dispose的释放流程刚好和Copy相反,Copy的操作顺序是:Block_layout -> Block_byref,而dispose的操作顺序是Block_byref -> Block_layout