32.关于Block你所该知道的一切

一、 block 的分类

全局 block堆 block栈 block

1.1 NSGlobalBlock 全局 block

  • 位于全局区

  • 未捕获任何变量 或 只捕获全局静态变量

未捕获任何变量

1

只捕获全局静态变量

4

1.2 堆 block

  • 位于堆区

  • block 内部使用 局部变量 或者 OC属性 ,并且赋值给 强引用 或者 Copy修饰 的变量

2

1.3 栈 block

  • 位于栈区

  • block 内部使用 局部变量 或者 OC属性 ,但 赋值给 强引用 或者 Copy修饰 的变量

3

1.4 面试题(一)

下面输出的结果会是什么样?

这里涉及到关于 block类型 的底层知识,我们到最后在进行解析。

1.5 面试题(二)

下面输出的结果会是什么样?

1.6 面试题(三)

下面输出的结果会是什么样?

二、 堆、栈block的生命周期

这里结合 二、三面试题 来进行探索。

2.1 面试题二解读

很可能有同学会给出只会打印 = 的答案。这就是对于 堆、栈block的生命周期 理解的不够深入。正确的输出如下

5

为什么呢?关键是对 blockB 的生命周期理解错误。一般会这么理解:

6

这里先不讲为什么,我们再看下 面试题三

2.2 面试题三解读

7

这里发生了野指针访问崩溃了。

2.3 堆 block 的生命周期

8

在大括号内部下断点可以看到, 这时 blockB 为 堆block 且有值。

9

赋值给 blockA

10

出了大括号后 blockA 为空了,说明 blockB 释放掉了。

因为作为 存储在堆上堆block 其生命周期仅存在于大括号内部。

2.4 栈 block 的生命周期

那么 面试题二 中的 blockB 的生命周期又是怎样的呢?

11

blockB栈block

12

赋值给 blockA

13

出了大括号的作用域了, blockA 的地址依旧没变,说明 blockB 没有被释放。

因为作为 存储在栈中栈block 其生命周期与 调用栈 相同。函数调用栈结束后释放。

三、 捕获变量的底层实现

3.1 普通变量的捕获

我们将上面的代码还原成 C++

14
  • block 的本质是一个结构体 __main_block_impl_0 ,如果有需要捕获的变量,结构体内部会生成一个成员用来存储,例子中是 int num;

  • __main_block_func_0 保存的是 block 的具体需要执行的函数

  • __block_implblock 的具体具体的一些数据,如函数等

  • __main_block_desc_0 是描述 block 特征的一个数据结构

这段代码中做了些什么呢?

一、 创建 block

1、 调用 __main_block_impl_0构造函数

2、 传入 __main_block_func_0 函数__main_block_desc_0_DATA 的地址变量 num

3、 指定 block 类型:impl.isa = &_NSConcreteStackBlock; (这里的堆block对应的是 _NSConcreteStackBlock

4、 存储标志位: impl.Flags = flags;

5、 存储函数: impl.FuncPtr = fp;

6、 描述内容赋值: Desc = desc;

二、 执行 block

1、 获取 __main_block_func_0 函数: ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)

2、 将 block 作为参数传入: ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

三、 执行函数 __main_block_func_0

1、 从 __main_block_impl_0 中取出变量 numint num = __cself->num;

2、 执行 NSLogNSLog((NSString *)&__NSConstantStringImpl__var_folders_py_7v1wvf813z5bmw0hr97yplwc0000gn_T_main_e543c2_mi_1, num);

3.2 __block 普通变量的捕获

将上面的代码转为 C++

15

我们发现多了几种数据类型: __Block_byref_num_0__main_block_copy_0__main_block_dispose_0

加了 __block 的变量,这里被用 __Block_byref_num_0 数据结构包装了一下,这里传入的 num 也是通过地址的形式传入的。

3.3 捕获实例变量

不活实例变量和 3.2 没有什么区别。

16

3.4 全局 block

17

3.5 栈 block

这里还原 C++ 你可能会遇到包错:

加上一些参数即可:

18
19

这里发现 __weak 实际是用 __attribute__((objc_ownership(weak))) 进行了包装,这里还没有什么更有用的线索,我们先继续探究。

3.6 _NSConcreteStackBlock

相信细心的你一定能发现一个问题,不论什么类型的 block ,还原出的 C++ 底层源码中都是 impl.isa = &_NSConcreteStackBlock; 看名字是个 栈 block 。和实际运行打断点运行时看到的不一样啊。下面我们就来分析一下这里发生了什么。

四、 运行时 block 类型的确定

由于这里并没有什么具体线索,我们通过下断点,并打开汇编。这里是一个 全局block

20

4.1 进入汇编断点

21

4.2 objc_retainBlock

22

通过 libobjc.A.dylib`objc_retainBlock: 发现它在 libobjc 中。

23

在 oc 源码中定位到了相关代码,但更进一步调用的 _Block_copy 并没有找到,我们继续回到汇编,继续执行下一步。

4.3 _Block_copy

24

这例进入了 _Block_copy 的汇编,发现它是 libsystem_blocks 中的,我们来 libclosure 中寻找一下线索,找到了下面的源码。

五、 汇编验证 block 的类型转换

5.1 全局block

25

一直来到 _Block_copy 内,读取 x0 寄存器 (ARM64下,x0这时存的是消息的接收者)。

26

进入 _Block_copy 时,就已经标识为 全局block 了。

我们发现输出了这样数据结构 signature invoke ,后面再具体看他们代表了什么。

5.2 堆block

27

进入 _Block_copy 时,显示的是 栈block 。

28

和源码所写的一样,这里进行了 malloc :

29

_Block_copy return 之前我们再看一下 x0 :

30

这里已经变成了 堆block

_block 捕获变量的变化

进入 _Block_copy 时:

31

_Block_copy return 之前

32

这里的数据结构又发生了变化,多了 copy dispose

5.3 栈block

33

通过查看调用栈,我们发现并没有调用 objc_retainBlock 。没有进行 _Block_copy 操作。

六、 Block_layout 结构分析

35

6.1 isa

block 的类型:全局、堆、栈

6.2 flags

这些标志位都标明了不同的使用时间,有 运行时:runtime编译期:compiler

6.3 reserved

保留字段

6.4 invoke

函数指针,即block中具体需要执行的内容。

6.5 descriptor

这里我们在看下其中的 Block_descriptor

36

这里就和我们之前输出的内容对应上了:

37

虽然 Block_layout 的内存结构中只有 Block_descriptor_1 ,但是通过解读源码,可以发现, Block_descriptor_2Block_descriptor_3 的读取是通过内存平移实现的,具体有哪些数据可读则是根据 标识位 来判别。

Block_descriptor_1

基础数据,一定有的

Block_descriptor_2

COPY 相关字段,有 BLOCK_HAS_COPY_DISPOSE 标志位时有。

Block_descriptor_3

签名相关数据,有 BLOCK_HAS_SIGNATURE 标志位时有。

七、 block 签名

上面的 Block_descriptor_3 中咱们提到了签名。如上面例子输出的 signature: "v8@?0" 很眼熟,它其实是 Type Encodings 类型编码,我们在 属性 中有遇到过。

  • v8@?0

    • v 表示返回值为空

    • 8 表示参数的总大小

    • @? 表示block,

    • 0 表示从0号字节开始

具体可以对照官方文档

NSMethodSignature

我们也可以通过 NSMethodSignature 来输出详细它的代表信息。

八、 __block 捕获变量的生命周期

38

我们再来研究下捕获变量的原理,我们继续通过汇编探索。

39

这里会调用到 _Block_object_assign

我们找到相关源码:

8.1 捕获变量类型

8.2 _Block_object_assign

8.3 _Block_byref_copy

__Block 捕获外界变量的操作 内存拷贝 等

这里我们遇到了用来包装捕获变量的 Block_byref 结构体,还有个一个 byref_keep 与生命周期相关的函数调用,但是他们的来源并没有什么线索,我们只能继续从 C++ 中寻找一些线索。

8.4 Block_byref

其中除了 Block_byref 还有 Block_byref_2Block_byref_3 。他们和前面的 Block_descriptor_1 一样,也是通过标志位区分,通过内存平移进行读写的

8.5 通过 C++ 理解 Block_byref 结构

位了更深入的理解 Block_byref 的结构,我们通过还原 C++ 找一下线索:

byref_keep

再次调用 _Block_object_assign (详见 8.2),这时传入的 flag131 ,二进制为 1000 0011,而 BLOCK_FIELD_IS_OBJECT = 3 ,所以会执行 BLOCK_FIELD_IS_OBJECT 相关的分支,进行 拷贝引用计数 + 1

byref_destroy

调用 _Block_object_dispose 进行销毁

九、 补充:面试题一解析

40

这里是 1 应该都没什么疑问

这里为什么是 3 呢?

  • 这是引用了一个局部变量

    • 内部创建了一个结构体对 obj 进行了 引用 ,引用计数+1

  • 这个 block强引用 ,它是一个 堆block

    • 从上面的源码学习中我们得知, block 的类型是运行时确定的

    • 而在这个过程中,进行了 _Block_copy,引用计数 +1

  • 所以这里打印出了 3

为什么是 4

  • 这是引用了一个局部变量

    • 内部创建了一个结构体对 obj 进行了 引用 ,引用计数+1

  • 这个 block弱引用 ,它是一个 栈block,引用计数不变

为什么是5

  • 将 栈blockB 进行了 copy,引用计数 +1

总结

__block 捕获变量进行了三重拷贝:

  • 变量A__block 修饰,就会被 blockB拷贝

    • _Block_copy

  • blockB 捕获 Block_byref C拷贝

    • _Block_object_assign - _Block_byref_copy

  • Block_byref C 对捕获的 变量A 进行拷贝

    • _Block_object_assign - byref_keep

参考

Last updated

Was this helpful?