iOS开发总结-Block(二)


前言

上一篇文章主要是介绍在iOS开发过程中遇到的Block的具体结构和类型,以及相关操作的影响。
这一篇文章主要是探究 Block 的实现,以及 __block 的原理。
注意:本文中实例都是在ARC环境下运行的。

作者使用的环境是:

  • Xcode 9正式版。
  • 真机调试 iPhone 6Plus 10.3.3
  • MacBook Pro 10.12.6

Block 的实现

关于block的结构,上一篇文章已经说过了,但是我们并没有具体的使用。
这次我将使用 clang 工具查看 Objective-C 代码用c语言的实现,并对block的具体实现进行分析。

需要注意的是clang转化的文件只能作为研究的参考,在实际运行中,还是和源文件有所区别的。

还是熟悉的代码:

typedef void(^TestBlockExample)(void);
    TestBlockExample block1 = ^{
        printf("Hello, World!\n");
    };

补充一下使用clang工具的命令(后面的BlockTest.m是具体的文件名):

clang -rewrite-objc BlockTest.m
iOS开发总结-Block(二)_第1张图片
image.png

代码太多,这里只取出关键部分进行分析。

从下面的选中部分可以看出来 block 主要分成三个部分:
__BlockTest__test_block_impl_0、
__BlockTest__test_block_func_0、
__BlockTest__test_block_desc_0_DATA

下面我们一个个来分析:
先看最简单的

  • __BlockTest__test_block_desc_0_DATA
static struct __BlockTest__test_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __BlockTest__test_block_desc_0_DATA = { 0, sizeof(struct __BlockTest__test_block_impl_0)};

从这里就可以看出其实这个结构体就包含两个 size_t 信息。一个是保留数 reserved ,默认就是0,另一个是 Block_size 就是 __BlockTest__test_block_impl_0 结构体的大小。这个地方没啥好说的,我们看下一个:

  • __BlockTest__test_block_func_0
static void __BlockTest__test_block_func_0(struct __BlockTest__test_block_impl_0 *__cself) {
        printf("Hello, World!\n");
    }

这一部分就相当于临时创建了一个函数,供block实际执行的时候调用。

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

这里面包含了一个 impl 是 __block_impl 结构体,定义如下:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

还有个 Desc 其实就是上面说的 __BlockTest__test_block_desc_0_Data,是block的size信息,下面 __BlockTest__test_block_impl_0 这也是个block结构,就相当于 __BlockTest__test_block_impl_0 的构造函数了。根据传进来的参数对 impl 和 Desc 进行赋值。

下面我们更改一下代码,让block捕获外部变量,根据上一篇的内容,我们已经预见了 block将会是 _ NSMallocBlock 类型,但是这里面的block实际上还是在栈中,所以isa指向是_NSConcreteStackBlock:

    int a = 6;
    TestBlockExample block1 = ^{
        printf("Hello, World! %d\n", a);
    };
iOS开发总结-Block(二)_第2张图片
image.png

我们看到其实多出来的也就是标注的几个地方,中间还贴心的给了注释 “bound by copy” 这里可以简单理解为“值拷贝”,实际上这个地方已经将变量a存到了block的结构体中,外部对变量a以后的修改都不会影响到block结构体中的a。下面讲到 __block 的时候还会探究这个地方。
言归正传,__BlockTest__test_block_impl_0 里面多出了 a 变量,其实这样看来,捕获一个没有被 __block 修饰的变量和普通的block区别不是很大。好,下面我们看一下 __block 的影响。

- (void)test {
    int a = 6;
    TestBlockExample block1 = ^{
        printf("Hello, World! %d\n", a);
    };
    a = 8;
    block1();
}

这段代码的运行结果就是

Hello, World! 6

下面我们改一下代码:

- (void)test {
    __block int a = 6;
    TestBlockExample block1 = ^{
        printf("Hello, World! %d\n", a);
    };
    a = 8;
    block1();
}

运行结果

Hello, World! 8

我们都知道这就是 __block 的功劳。那具体 __block 做了什么呢,我们用clang看一下:

image.png

简直了,没想到一个__block做了这么多改变,我做了个文件比对:

iOS开发总结-Block(二)_第3张图片
image.png

使用__block以后大致改变的就是:

  • 变量a的类型变了
__block 前 :int a;
__block 后 :__Block_byref_a_0 *a; // by ref

这里面的__Block_byref_a_0就是下面的这个结构体:

  • 增加了新的结构体
struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

新增的结构体也可以看成是一个对象。
在 __Block_byref_a_0 结构体中我们可以看到成员变量__forwarding,它持有指向该实例自身的指针;
另外还有变量a,变量a在这里只是一个成员了;
这就相当于比之前不加__block修饰的时候多了4个成员变量。

  • 构造的时候 入参变了
__block 前 :__BlockTest__test_block_impl_0(void *fp, struct __BlockTest__test_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
__block 后 :__BlockTest__test_block_impl_0(void *fp, struct __BlockTest__test_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {

主要是 传入的变量值由 _a 变成了 _a->__forwarding,这有什么意义呢,上面说了__forwarding持有指向__Block_byref_a_0实例的指针,这为访问和修改外部变量创造了条件。

  • 生成函数体的时候(__BlockTest__test_block_func_0)
__block 前 :
int a = __cself->a; // bound by copy 值拷贝
        printf("Hello, World! %d\n", a); // 使用的时候直接使用值

__block 后 :
__Block_byref_a_0 *a = __cself->a; // bound by ref (引用)地址拷贝
        printf("Hello, World! %d\n", (a->__forwarding->a));// 使用的时候需要通过__forwarding指针访问变量

这个地方一开始我觉得多此一举,因为直接访问地址不行么,为啥还要加个__forwarding指针“中转”一下,后来也是查阅了一些资料:
其实这样做的目的是防止栈中的block在出栈的时候,其所持有的变量也会被回收,这样在栈中所做的修改也不会保存到堆里,__block也就没了意义。所以在block被copy到堆中的时候,其实栈中的__forwarding的指向也随之改变,和堆中的__forwarding同时指向了堆中的实例(Block_byref_a_0)。

这样无论是栈中的 block 或者堆中的 block,各自使用 __forwarding 来修改变量,就都可以保留下来了。

在栈中的时候大致是这样的情况:

iOS开发总结-Block(二)_第4张图片
image.png

copy到堆中的时候的情况:

iOS开发总结-Block(二)_第5张图片
image.png
  • 增加了两个函数 copy 和 dispose
static void __BlockTest__test_block_copy_0(struct __BlockTest__test_block_impl_0*dst, struct __BlockTest__test_block_impl_0*src) {
_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static void __BlockTest__test_block_dispose_0(struct __BlockTest__test_block_impl_0*src) {
_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}

这两个函数类似于MRC 中retain和release的效果。

  • __BlockTest__test_block_desc_0_DATA 的sizeof 计算block的成员增加了
__block 前 :
__BlockTest__test_block_desc_0_DATA = { 0, sizeof(struct __BlockTest__test_block_impl_0)};

__block 后 :
__BlockTest__test_block_desc_0_DATA = { 0, sizeof(struct __BlockTest__test_block_impl_0), __BlockTest__test_block_copy_0, __BlockTest__test_block_dispose_0};
  • 变量声明时的变化
__block 前 :int a = 6;

__block 后 :__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 6};

后面增加的这些就是为了给__Block_byref_a_0填充数据,以备后续修改。

  • 初始化block的变化
__block 前 :
TestBlockExample block1 = ((void (*)())&__BlockTest__test_block_impl_0((void *)__BlockTest__test_block_func_0, &__BlockTest__test_block_desc_0_DATA, a));

__block 后 :
TestBlockExample block1 = ((void (*)())&__BlockTest__test_block_impl_0((void *)__BlockTest__test_block_func_0, &__BlockTest__test_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));

真正的变化就是 最后一个参数 ‘a’ 变成了 ‘(__Block_byref_a_0 *)&a, 570425344’
不过这后面的 ‘570425344’ 我一直不明白是什么意思。

  • 修改变量的变化
__block 前 :a = 8;
__block 后 :(a.__forwarding->a) = 8;

a.__forwarding指向了自身。
在block构造的时候我们用到了a.__forwarding,
在使用的时候(__BlockTest__test_block_func_0)也是通过(a->__forwarding->a)这样的方式找到最终修改过的数值‘8’。
能做到这样的原因上面已经说过了,这里复述一下:
栈上的__block变量复制到堆上时,会将成员变量__forwarding的值替换为复制到堆上的__block变量用结构体实例的地址。所以“不管__block变量配置在栈上还是堆上,都能够正确的访问该变量”,这也是成员变量__forwarding存在的理由。参照上面的图。。。

好了,到这里__block的作用已经讲的差不多了,剩下的更深层次的东西,暂时不知道怎么入手,其实关于block的知识还有很多,比如在MRC的情况下是怎么样的,还有静态变量和全局变量之类的,本文都没有讨论,下次有机会在研究吧,另外下面参考的几篇文章中也有相关的介绍。


参考的相关资料

Block源码解析和深入理解
关于Block再啰嗦几句
深入理解Block之Block的类型
深入研究Block捕获外部变量和__block实现原理
iOS Block源码分析系列(二)————局部变量的截获以及__block的作用和理解

你可能感兴趣的:(iOS开发总结-Block(二))