iOS底层 - 谈Objective-C block 的三层拷贝 (完结篇)

1,187 阅读16分钟

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

写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。

目录如下:

  1. iOS 底层原理探索 之 alloc
  2. iOS 底层原理探索 之 结构体内存对齐
  3. iOS 底层原理探索 之 对象的本质 & isa的底层实现
  4. iOS 底层原理探索 之 isa - 类的底层原理结构(上)
  5. iOS 底层原理探索 之 isa - 类的底层原理结构(中)
  6. iOS 底层原理探索 之 isa - 类的底层原理结构(下)
  7. iOS 底层原理探索 之 Runtime运行时&方法的本质
  8. iOS 底层原理探索 之 objc_msgSend
  9. iOS 底层原理探索 之 Runtime运行时慢速查找流程
  10. iOS 底层原理探索 之 动态方法决议
  11. iOS 底层原理探索 之 消息转发流程
  12. iOS 底层原理探索 之 应用程序加载原理dyld (上)
  13. iOS 底层原理探索 之 应用程序加载原理dyld (下)
  14. iOS 底层原理探索 之 类的加载
  15. iOS 底层原理探索 之 分类的加载
  16. iOS 底层原理探索 之 关联对象
  17. iOS底层原理探索 之 魔法师KVC
  18. iOS底层原理探索 之 KVO原理|8月更文挑战
  19. iOS底层原理探索 之 重写KVO|8月更文挑战
  20. iOS底层原理探索 之 多线程原理|8月更文挑战
  21. iOS底层原理探索 之 GCD函数和队列
  22. iOS底层原理探索 之 GCD原理(上)
  23. iOS底层 - 关于死锁,你了解多少?
  24. iOS底层 - 单例 销毁 可否 ?
  25. iOS底层 - Dispatch Source
  26. iOS底层 - 一个栅栏函 拦住了 数
  27. iOS底层 - 不见不散 的 信号量
  28. iOS底层 GCD - 一进一出 便成 调度组
  29. iOS底层原理探索 - 锁的基本使用
  30. iOS底层 - @synchronized 流程分析
  31. iOS底层 - 锁的原理探索
  32. iOS底层 - 带你实现一个读写锁
  33. iOS底层 - 谈Objective-C block的实现(上)
  34. iOS底层 - 谈Objective-C block的实现(下)

以上内容的总结专栏


细枝末节整理


前言

今天,我们从底层结构开始,揭开Block的面纱,从流程开始,解释上一篇中面试题的疑问。

Block 底层

欲探寻Block底层,必要将其编译。

我们自定义一个block:

image.png

xcrun 一下,将其编译成cpp文件,在看其底层实现和底层结构:

static void _I_BlockViewController_viewDidLoad(BlockViewController * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("BlockViewController"))}, sel_registerName("viewDidLoad"));


    int superman = 100;

    //__BlockViewController__viewDidLoad_block_impl_0 函数的调用
    void(*myBlock)(void) = ((void (*)())&__BlockViewController__viewDidLoad_block_impl_0((void *)__BlockViewController__viewDidLoad_block_func_0,
        &__BlockViewController__viewDidLoad_block_desc_0_DATA,
        superman));
    
    
    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);

    /** 上面两行我们将类型去掉整理一下
     
     void(*myBlock)(void) = __BlockViewController__viewDidLoad_block_impl_0(
        __BlockViewController__viewDidLoad_block_func_0,
        __BlockViewController__viewDidLoad_block_desc_0_DATA,
        superman );
     
     myBlock->FuncPtr(myBlock);
     
     */

}

block_impl

可以看到 myBlock 等于 __BlockViewController__viewDidLoad_block_impl_0,那么,我们找一下其实现的内容,:

struct __BlockViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __BlockViewController__viewDidLoad_block_desc_0* Desc;
  int superman;
  __BlockViewController__viewDidLoad_block_impl_0(void *fp,
      struct __BlockViewController__viewDidLoad_block_desc_0 *desc, 
      int _superman, int flags=0) : superman(_superman) {
      
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
    
  }
};

它是一个结构体,这个结构体就等于 结构体的构造函数;构造函数传三个参数,最后一个是 superman 并且结构体中有一个成员就是 superman (在构造函数中将传参过来的 superman 赋值给了 结构体成员的 superman ),这里很奇怪,和我们自定义的变量名一样。

这里会不会是Block根据捕获的外部变量自动生成的成员(并在构造函数中完成赋值)呢?

我们验证下这个猜想:

将 myBlock 改为如下内容:

image.png

xrun 之后:

这里少了一个参数,结构体也没有了 superman 那个成员:

  void(*myBlock)(void) = ((void (*)())&__BlockViewController__viewDidLoad_block_impl_0(
  (void *)__BlockViewController__viewDidLoad_block_func_0, 
  &__BlockViewController__viewDidLoad_block_desc_0_DATA));

    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);

...

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

我们再进一步做一下验证,来证明我们的猜想是对的:

image.png

同样的 xcrun 一下看结果:

    AppDelegate *app = ((AppDelegate *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("alloc"));
    void(*myBlock)(void) = ((void (*)())&__BlockViewController__viewDidLoad_block_impl_0((void *)__BlockViewController__viewDidLoad_block_func_0, &__BlockViewController__viewDidLoad_block_desc_0_DATA, app, 570425344));

    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    
...

struct __BlockViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __BlockViewController__viewDidLoad_block_desc_0* Desc;
  AppDelegate *app;
  __BlockViewController__viewDidLoad_block_impl_0(void *fp, struct __BlockViewController__viewDidLoad_block_desc_0 *desc, AppDelegate *_app, int flags=0) : app(_app) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

isa

通过编译的代码,我们也可以看到 在编译阶段,Block是一个栈Block (impl.isa = &_NSConcreteStackBlock;); 但是,我们前两片关于Block的总结,Block捕获外部变量后,并且不是弱引用,这种情况的Block是一个堆Block。也就是说,

在编译时 先编译成了栈 Block, 到之后的 运行时 才会变成堆Block

那么是怎么变成堆Block的呢? 这也是我们接下来要继续探索的内容;

FuncPtr

在构造函数中,我们传参的第一个 fp 赋值给了 FuncPtr ; 传进去的是 __BlockViewController__viewDidLoad_block_func_0 是一个函数:

static void __BlockViewController__viewDidLoad_block_func_0(struct __BlockViewController__viewDidLoad_block_impl_0 *__cself) {
  int superman = __cself->superman; // bound by copy


        NSLog((NSString *)&__NSConstantStringImpl__var_folders_w3_ptw7ymvs5pjf98xqvjnp86wc0000gn_T_BlockViewController_385088_mi_0, superman);
    }

随后就在下一行,进行了 FuncPtr 的调用执行。

Block的成员变量FuncPtr在构造函数中对要执行的内容 进行了保存,如果没有调用,那,就不会调用方法执行。 所以这也就是一定要调用执行的原因

方法内部
  int superman = __cself->superman; // bound by copy

__cself 就是 __BlockViewController__viewDidLoad_block_impl_0 ( 因为 myBlock->FuncPtr(myBlock); )

那么,这里的 int superman 和 结构体内部的 superman 是一样(值相同,地址不同);

__block 做了什么

下面我们 加上 __block 在试一下:

  • 在Block结构体内部: 之前是:

int superman;

加入 __block 后:

__Block_byref_superman_0 *superman; // by ref
  • viewDidLoad 函数内部调用中: 之前是:

int superman = 100;

加入 __block 后:


// 结构体的初始化
// 编译器省略了一行代码: int superman = 100;
__Block_byref_superman_0 superman = {
    (void*)0,
    (__Block_byref_superman_0 *)&superman, 
    0,
    sizeof(__Block_byref_superman_0),
    100
    };

//__attribute__((__blocks__(byref))) __Block_byref_superman_0 superman = {(void*)0,(__Block_byref_superman_0 *)&superman, 0, sizeof(__Block_byref_superman_0), 100};

我们看下 这个结构体 __Block_byref_superman_0

struct __Block_byref_superman_0 {
  void *__isa;
__Block_byref_superman_0 *__forwarding;
 int __flags;
 int __size;
 int superman;
};

可以看到是将 &superman 赋值给了 __Block_byref_superman_0 结构体 superman__forwarding 成员。

*__forwarding = &superman(100)

在 Block 结构体内部的构造函数中: 将 _superman->__forwarding 赋值给了 Block 内部 成员变量 superman ;

struct __BlockViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __BlockViewController__viewDidLoad_block_desc_0* Desc;
  __Block_byref_superman_0 *superman; // by ref
  __BlockViewController__viewDidLoad_block_impl_0(void *fp, struct __BlockViewController__viewDidLoad_block_desc_0 *desc, __Block_byref_superman_0 *_superman, int flags=0) : superman(_superman->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__block 整理

上述的流程是,在 添加了 __block 之后:

  • 原先的变量类型从 int 变为了 结构体 __Block_byref_superman_0 ;(我们定义的 100 取地址存放在结构体成员变量 *__forwarding 中 取地址存放在指针中 --- *__forwarding 指向 superman 的地址 )

  • 原先是将 superman 赋值给block结构体成员superman,变成了将 _superman->__forwarding指针 赋值给 block 成员 *superman指针 ;

  • 最后我们再看 FuncPtr 方法的调用也就是 __BlockViewController__viewDidLoad_block_func_0 :(方法内部 __cself 是Block 本身

原来是 :

// 这里的 superman 和 我们 Block 中的 superman 不一样哦
// 值相同,地址不同 - 值拷贝
int superman = __cself->superman; // bound by copy

加入 __block 后:

    // 这里的 superman 就和 我们外界 __block 的 superman 是相同
    // 这里是传的地址进来,是一个指针赋值 (整理中的第一条)
    // 所以这里的 superman 是可以修改的
    __Block_byref_superman_0 *superman = __cself->superman; // bound by ref
    (superman->__forwarding->superman)++;
  • __block 做了下面这些操作 :

生成了结构体__Block_byref_superman_0,并且传给 Block 的是指针地址,就达到了修改同一片内存空间的效果。

总结

  • Block 会根据捕获的外部变量自动生成相应的成员变量(并在构造函数中完成外部变量赋值给成员变量的操作)
  • 外界变量加了 __block 修饰后 ,变量会在底层 生成了结构体__Block_byref_superman_0,并且传给 Block 内部的__Block_byref_superman_0*superman指针的是指针地址,如此,经__block修饰后的变量和Block内部的变量指向同一片内存空间, 就达到了修改同一片内存空间的效果。

Block底层拷贝

我们在 __BlockViewController__viewDidLoad_block_func_0 这个函数附近还看到了一些方法 如(下面 为了表述方便我们 用 xxx 替代 __BlockViewController__viewDidLoad_block ):

xxx_copy_0xxx_dispose_0 xxx_desc_0 以及在 xxx_copy_0 中的 _Block_object_assign 等,这些都是什么呢? 还有就是上一节我们分析 为什么在编译时是 栈Block, 到运行时的时候才会是 堆Block ,运行时处理了哪些呢? 接下来我们就探索以上这些问题。

既然要探索运行时的Block是如何将编译时的栈Block操作成堆Block的,那么,我们那就将代码运行起来,跑在真机上断点调试一下。

image.png

打开汇编看汇编流程:

image.png

这里来到了 objc_retainBlock 这里。

在objc源码中:

id objc_retainBlock(id x) {
    return (id)_Block_copy(x);
}

...

// 创建一个基于堆的Block副本,或者简单地添加一个对现有Block的引用 
// 这必须与Block_release配对以恢复内存,即使在运行时也是如此 
// 在Objective-C的垃圾收集。
BLOCK_EXPORT void *_Block_copy(const void *aBlock)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
    
// 丢失引用,如果是基于堆的和最后的引用,则恢复内存
BLOCK_EXPORT void _Block_release(const void *aBlock)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);


// 由编译器使用。不要自己调用这个函数
BLOCK_EXPORT void _Block_object_assign(void *, const void *, const int)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

// 由编译器使用。不要自己调用这个函数
BLOCK_EXPORT void _Block_object_dispose(const void *, const int)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

// 由编译器使用。不要自己调用这个函数
BLOCK_EXPORT void * _NSConcreteGlobalBlock[32]
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
BLOCK_EXPORT void * _NSConcreteStackBlock[32]
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
    

经过一番寻找,我们在 libclosure 中找到了 _Block_copy

_Block_copy

// Copy, or bump refcount, of a block.  If really copying, call the copy helper if present.
void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;

    if (!arg) return NULL;
    
    // The following would be better done as a switch statement
    aBlock = (struct Block_layout *)arg;
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    else {// 栈 - 堆 (编译期间生成的栈Block)
        // Its a stack block.  Make a copy.
        size_t size = Block_size(aBlock);
        struct Block_layout *result = (struct Block_layout *)malloc(size);
        if (!result) return NULL;
        memmove(result, aBlock, size); // bitcopy first
#if __has_feature(ptrauth_calls)
        // Resign the invoke pointer as it uses address authentication.
        result->invoke = aBlock->invoke;

#if __has_feature(ptrauth_signed_block_descriptors)
        if (aBlock->flags & BLOCK_SMALL_DESCRIPTOR) {
            uintptr_t oldDesc = ptrauth_blend_discriminator(
                    &aBlock->descriptor,
                    _Block_descriptor_ptrauth_discriminator);
            uintptr_t newDesc = ptrauth_blend_discriminator(
                    &result->descriptor,
                    _Block_descriptor_ptrauth_discriminator);

            result->descriptor =
                    ptrauth_auth_and_resign(aBlock->descriptor,
                                            ptrauth_key_asda, oldDesc,
                                            ptrauth_key_asda, newDesc);
        }
#endif
#endif
        // reset refcount
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        _Block_call_copy_helper(result, aBlock);
        // Set isa last so memory analysis tools see a fully-initialized object.
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}

...

struct Block_layout {
    void * __ptrauth_objc_isa_pointer isa; //栈or堆 Block
    volatile int32_t flags; // 包含参考数(标识)
    int32_t reserved; //流程的数据
    BlockInvokeFunction invoke; // 函数的调用
    struct Block_descriptor_1 *descriptor; // 描述(是否正在析构、是否有keep函数等)
    // imported variables
};

...

// 用于描述块对象的Block_layout->标志的值
enum {
    BLOCK_DEALLOCATING =      (0x0001),  // runtime
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
    BLOCK_INLINE_LAYOUT_STRING = (1 << 21), // compiler

#if BLOCK_SMALL_DESCRIPTOR_SUPPORTED
    BLOCK_SMALL_DESCRIPTOR =  (1 << 22), // compiler
#endif

    BLOCK_IS_NOESCAPE =       (1 << 23), // compiler
    BLOCK_NEEDS_FREE =        (1 << 24), // runtime
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
    BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code
    BLOCK_IS_GC =             (1 << 27), // runtime
    BLOCK_IS_GLOBAL =         (1 << 28), // compiler
    BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
    BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler
    BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler
};

我们读取一下 x0 寄存器,看一下消息的接受者。

image.png

是一个全局Block。

下面我们捕获一下外部的变量:

image.png

断点读取一下x0 :

image.png

这里是一个栈Block。

我们在汇编代码的最后,在 ret 那一行打上断点,再看 x0寄存器(函数返回值存放在这里):

image.png 此时这里是一个堆Block,我们经过了 _Block_copy 函数之后,就从栈Block成为了堆Block;

如此: 编译器 编译完代码后, 只生成了 栈Block ,因为,在编译期 没有 alloc 的重定义处理,且需要申请内存空间一系列的处理, 堆内存的开辟操作 放在编译期 大大增加了编译器的压力,因此, 编译期间 没有 堆Block 只会默认标记一个 栈Block ;等到运行时 经过 判断是 栈Block 并且 捕获了外部变量 就会将 此 栈Block 完整的拷贝一份 到 堆空间, Block 的isa 再 指定为 堆Block

isa 重新标记

_Block_copy 函数流程中, 如果 是 需要释放的Block或者是一个全局的Block会返回出去,接下来 编译期生成的 栈Block 并且捕获了外部变量的话 会进行一个拷贝(按大小开辟内存空间,memmove 拷贝一份到result返回值,对签名、flage等进行处理,isa 重新标记为 _NSConcreteMallocBlock 堆Block ),

在打印 x0寄存器堆时候,我们打印出了以下信息:

(lldb) po 0x0000000281238e10
<__NSMallocBlock__: 0x281238e10>
 signature: "v8@?0"
 invoke   : 0x1028bde58 (/private/var/containers/Bundle/Application/CFEA7FC5-6956-47CE-B759-AF1F32E7DDD5/bb.app/bb`__29-[ViewController viewDidLoad]_block_invoke)
 copy     : 0x1028bde90 (/private/var/containers/Bundle/Application/CFEA7FC5-6956-47CE-B759-AF1F32E7DDD5/bb.app/bb`__copy_helper_block_e8_32s)
 dispose  : 0x1028bdec8 (/private/var/containers/Bundle/Application/CFEA7FC5-6956-47CE-B759-AF1F32E7DDD5/bb.app/bb`__destroy_helper_block_e8_32s)

签名

这里的 invoke 是函数的调用者, signature 是Block的签名,我们验证下:

image.png

1个参数,无返回值(v),参数从 0 号位开始, 是一个 对象类型,也是一个Block类型 (@?)

(我们为什么要关注这个签名呢? 如果进行底层的copy或hook的话,需要我们在想要调用的地方invoke一下, 这其实是一层消息,涉及到消息机制,消息流程的最后一个阶段 必须要获取到签名,才能进行 invocation。)

这里对于签名以及isa我们大致了解了;但是下面的 copy函数 dispose函数 这两个是什么呢?

Block结构

从上面的 _Block_copy 流程,在最后对 isa 进行了赋值,我们看下Block的结构

struct Block_layout {
    void * __ptrauth_objc_isa_pointer isa; //栈or堆 Block 
    volatile int32_t flags; // 包含参考数(标识)
    int32_t reserved; //流程的数据 
    BlockInvokeFunction invoke; // 函数的调用

    // 描述(是否正在析构、是否有keep函数等) 
    // 从descriptor 开始是可选参数(内存的连续可选)
    struct Block_descriptor_1 *descriptor; 
    // imported variables 
};

我们发现如果定义类型为 BLOCK_DESCRIPTOR_2 ,才会有 copy 和 dispose。

#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    BlockCopyFunction copy;
    BlockDisposeFunction dispose;
};

我们就去定位下 什么时候是 BLOCK_DESCRIPTOR_2 这个类型,却发现,太多,找不到。

我们没找到其如何生成的(可能是编译器帮我们搞定的就省略了),但是我们可以知道其如何get到的。如下:

static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock)
{
    uint8_t *desc = (uint8_t *)_Block_get_descriptor(aBlock);
    desc += sizeof(struct Block_descriptor_1);
    return (struct Block_descriptor_2 *)desc;
}

通过 _Block_get_descriptor += Block_descriptor_1 就得到了 Block_descriptor_2 什么意思呢? 如下图所示(因为内存连续,通过内存平移得到 Block_descriptor_2 以此类推 Block_descriptor_3 也可以通过内存平移获得):

Block_descriptor_2.001.jpeg

Block_descriptor_3 中有 signature 和 layout;

#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 必然有 Block_descriptor_3 和 Block_descriptor_2 。

既然这样,我们可以通过 x/10gx 来看一下内存:

image.png 上面这张图完美的诠释了 Block 的内存结构, 有 invoke descriptor (copy dispose) signature 这些数据,且全部打印出来一一验证。

捕获变量

前面的探索中,关于 _Block_copy 是一个栈拷贝到堆过程,但是通过 xcrun 我们在编译后的代码中发现了一个 _Block_object_assign ,如下:

__BlockViewController__viewDidLoad_block_copy_0

static void __BlockViewController__viewDidLoad_block_copy_0(struct __BlockViewController__viewDidLoad_block_impl_0*dst, struct __BlockViewController__viewDidLoad_block_impl_0*src) {

_Block_object_assign((void*)&dst->superman, 
(void*)src->superman,
 8/*BLOCK_FIELD_IS_BYREF*/);

}

这里有点奇怪,

这两个 copy 并没有什么关系的

这里其实是传参 我们上面图片中 红色框框住的 copy ;如下:

static struct __BlockViewController__viewDidLoad_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  
  //这里 这里
  void (*copy)(struct __BlockViewController__viewDidLoad_block_impl_0*, struct __BlockViewController__viewDidLoad_block_impl_0*);
  
  
  void (*dispose)(struct __BlockViewController__viewDidLoad_block_impl_0*);
} __BlockViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __BlockViewController__viewDidLoad_block_impl_0),

  //这里 这里
__BlockViewController__viewDidLoad_block_copy_0, 

__BlockViewController__viewDidLoad_block_dispose_0};

Block 其实就是一个对象,我们之前是对其 初始化赋值的一个过程。其中有一个 Block_descriptor_1 , 还有一个Block_descriptor_2 ( 2 里面有 copy 和 dispose 函数 , 初始化的时候,也要初始化,就是给他们一个函数的实现。) 也就是上面的__BlockViewController__viewDidLoad_block_copy_0,就是copy函数的实现。

在 copy 函数的实现过程中 有调用一个 _Block_object_assign

源码定位后发现下面的注释内容:

是对捕获变量的类型的处理

The flags parameter of _Block_object_assign and _Block_object_dispose is set to

    * BLOCK_FIELD_IS_OBJECT (3), for the case of an Objective-C Object,

    * BLOCK_FIELD_IS_BLOCK (7), for the case of another Block, and

    * BLOCK_FIELD_IS_BYREF (8), for the case of a __block variable.

If the __block variable is marked weak the compiler also or's in BLOCK_FIELD_IS_WEAK (16)

验证一下:

我们这里是一个 8

是因为我们这个例子是 __block 修饰的变量。 image.png

下面我们修改为一个OC对象来试一下:

image.png

下面,我们就进入到 _Block_object_assign 方法的实现。

_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:
        /*******
        id object = ...;
        [^{ object; } copy];
        ********/
        // 普通的对象类型 直接 retain 操作
        // _Block_retain_object_default = dose nothind
        // 交给系统级别默认的 arc 操作
        // _Block_retain_object_default = fn (arc)
        _Block_retain_object(object);
        // block外的对象object的值 赋值给 block 中的变量 -- 
        // 他们指向相同的内存空间 它们的地址并不一样
        *dest = object;
        break;

      case BLOCK_FIELD_IS_BLOCK:
        /*******
        void (^object)(void) = ...;
        [^{ object; } copy];
        ********/
               
        // 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];
         ********/

        *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

__block 修饰的在这里会按照原来的对象大小,开辟内从空间将值赋值过去,原来的forwarding和创建的forwarding 是一样的。

这个forwarding 最终是赋值给了block函数执行中的那个变量。

这里结合在Block底层那一小节中的 整理 来看,你的思路会更清晰一些。

这也就是为什么 __block 修饰的变量它们( block实现体外 的变量 和 内部的变量 )是同一片内存空间,它是指针拷贝。就是下面两行代码解释的很清楚:

  • copy->forwarding = copy;
  • src->forwarding = 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;
}

byref_keep

struct Block_byref_2 {
    // requires BLOCK_BYREF_HAS_COPY_DISPOSE
    BlockByrefKeepFunction byref_keep; //= __Block_byref_id_object_copy_131
    BlockByrefDestroyFunction byref_destroy; // = __Block_byref_id_object_dispose_131
};

这两个Function等于什么呢?-- Block_byref

// __block 修饰的变量   编译后会  是一个结构体

// 结构体
struct Block_byref {
    void * __ptrauth_objc_isa_pointer isa; // 8
    struct Block_byref *forwarding;  // 8
    volatile int32_t flags; // contains ref count//4
    uint32_t size; // 4
};

这里结合在Block底层那一小节中的 整理 来看,你的思路会更清晰一些。

__block 修饰的对象就是如下的一个结构体

__Block_byref_superman_0

struct __Block_byref_superman_0 {
  void *__isa;
__Block_byref_superman_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSObject *superman;
};

viewDidLoad中 对结构体 赋值如下 :

image.png

byref_keep 函数赋值的分别就是 :

__Block_byref_id_object_copy_131,
__Block_byref_id_object_dispose_131,

copy 函数调用的就是 如下内容:

// block 捕获的是  ->  Block_byref  捕获的是  ->  Object对象
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
 _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}

这里函数的 (char*)dst + 40 是什么意思呢?

这里要看回我们的结构体 __Block_byref_superman_0 本身了,内存平移 40字节 后 就是 其内部成员 NSObject *superman; 也就是copy函数传参的是 Object 然后这里会去到_Block_object_assign

先进入到 BLOCK_FIELD_IS_BYREF 再进入到 BLOCK_FIELD_IS_OBJECT

补充

在探索 __weak 修饰的block 的时候 还是一个 8 ,为什么呢?

其实在 _Block_object_assign 源码中有细节:

    case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
    case BLOCK_FIELD_IS_BYREF:

总结

一个被 __block 修饰的变量 会在编译时编译为 Block_byref 类型的结构体:


  1. block 进行一下 copy --> 从空间 拷贝空间;
  2. 接下来 block 会进行捕获变量 Block_byref --> 对 整个Block_byref对象 进行 拷贝 (整个的对象)
  3. Block_byref 在 copy 函数调用时 对内部的 object 成员 进行 拷贝

综上, 这就是 __block 修饰的变量的三层拷贝 。

  • block 捕获变量的时候会 对类型进行划分;
  • Block_byref(结构体) 类型 进行拷贝保存, 通过 byref_keep 函数 对内部的objec 对象进行保存;
  • 释放函数 大体也是一个逆向的过程 ( Block_byref(结构体)会进行一个 byref_destroy 那个释放函数 类似copy
static void __Block_byref_id_object_dispose_131(void *src) {
 _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}