Tino又想吃肉了

Block的定义与实现

Word count: 2.8kReading time: 10 min
2021/12/23

记录对Block的结构探究(不涉及libclosure)

Block在Objective-C中的定义与实现

Block,即闭包。在各种语言中或多或少都有着闭包的概念,在swift中Block即swift的closure。实际上闭包是一个定义相对简单的概念,我们可以把它理解为一段延后执行的代码块或一个匿名的函数,它的作用是保存了一个由多段代码组合而成的代码块并在之后以供调用。而在Objective-C中,Block的实现实际上也是一个对象(Block的结构中也存在一个ISA指针,在OC中具有ISA指针的结构体可以在广义上把它们划分为对象)。既然Block的底层实现是对象,那么必然会涉及到OC中的内存管理,因此OC中Block的实现和底层原理相对于swift中的closure也会更加复杂,值得写一篇记录来总结一下。

Block定义

  • 首先强调一点,Block在Objective-C中的底层实现是一个对象,也就是说它也有一个ISA指针。

首先在代码中定义两个block,然后将他们转成cpp代码。
定义block的语法是 返回值(^your_block_name)(argus) = ^{};

-w515

可以看到我们在这里定义了两个block,分别是myTestBlock1myTestBlock2。通过Clang将他们转成cpp代码后,代码被转换成了这样。

-w562

struct __main_block_impl_0

在上图转换后的代码中,可以发现一个__main_block_impl_0结构,和一个__main_block_impl_1结构,我们将代码往上拉,可以发现这两个结构的定义。

-w586
-w580

可以看到这两个结构非常类似,他们共同拥有一个struct __block_impl类型的属性impl,区别的地方是struct __main_block_desc_1struct __main_block_desc_0,从命名可以看出他们的区别并不大,实际上这个类的命名是在编译时根据block定义的顺序来命名的。

最后就是一个struct __main_block_impl_0/1结构体本身的构造函数,也就是说struct __main_block_impl_0/1在目前只包含了三个部分的内容。

在构造函数中,可以看到impl.isa = &_NSConcreteStackBlock这一行代码,可以知道block在定义时是默认定义为stack block,在后期如果有需要才copy成heap block。

同时,我们也注意到impl.FuncPtr = fp这行代码,fp是一个void*类型的指针,这行代码将该block需要执行的函数指针赋给了impl.FuncPtr,也就是说在block执行时是通过该函数指针来调用起需要执行的代码块。

struct __main_block_desc_x

我们先研究不相同的地方,看一看struct __main_block_desc_x的结构以及它的作用。

-w484

可以看到,这个结构体保存了block的信息,其中第一个值为reserved,即待用值。
第二个值为Block_size,即block的大小。 可以看到在下面的构造函数中,reserved被默认置为0,Block_size被置为sizeof(struct __main_block_impl_0)

struct __block_impl

接着我们来看两个block中都共同拥有的值struct __block_impl

-w301

可以看到,这个就是所有block的基本结构,在每个自定义的block对象中都有一个struct __block_impl类型的值。

struct __block_impl中,有四部分的内容

  • isa:isa指向说明了block在内存中的存放位置,即区别栈/堆/全局block
  • Flags:标示位,在初始化时被默认置为0
  • Reserved:留存位,留待备用
  • FuncPtr:函数指针,在初始化时block要执行的代码块的指针被赋予FuncPtr

以上就是block在OC中的定义,当然如果查看libclosure库,可以看到block的更底层的定义和实现。

Block分类

在开发中,我们常听到block根据它在内存中的分布被分为栈区block、堆区block、全局block。

  • NSStackBlock:栈区block,实际上该类block已经很少见了。block在编译时都会被指向成栈区block,但如果没有捕获__block修饰的变量,在运行后栈区block基本都被转换成了全局block。目前在Xcode12的测试环境中,只有在例如NSlog函数中直接定义一个匿名的block作为参数,这种情况的block才是栈区block
  • NSMallocBlock:堆区block,在block中引用了局部变量或被__block修饰的变量,或者对block调用了block_copy,此时block会被拷贝到堆上,成为堆区block
  • NSGlobalBlock:全局block,block内部不引用变量或只引用了全局变量或静态变量

现在将之前定义的block改写一下,并新增一个引用了被__block修饰变量的block。

-w613

进入lldb,可以看到他们的类型分别为

-w407

可以看到,引用了局部变量a的block1变成了堆block,引用了全局变量globalA的block2成了全局block,引用了被__block修饰变量的block3成了堆block。

捕获变量后的block

接着来分别看看捕获变量后的block的底层机构变成了什么,依旧是使用clang将代码转换为cpp代码

-w535

可以看到,被__block修饰的blockedA变量被封装成了__Block_byref_blockedA_0类型,先来看看这个是什么。

-w426

struct __Block_byref_blockedA_0中,有这么几个值

  • __isa:该值在初始化时被置为0的指针
  • __forwarding: 在初始化时,被__block修饰的变量的地址被强转成__Block_byref_blockedA_0,赋给了__forwarding,通过在此处by ref的注释我们知道,block在捕获该变量时是通过捕获地址的方式,所以才可以在block内部修改它的内容
  • __flags:标示位
  • __size:占用大小
  • blockedA: 存放被修饰的变量的真实的值,在这里为2

NSMallocBlock

首先看一下它的构造函数。

-w558

可以发现与前文中没有捕获变量的block结构已经不一样了。在构造函数传入的参数中多了一个__Block_byref_blockedA_0 *类型的&blockedA,和一个长整数。

  • 其中的&blockedA,并不是我们在代码中定义的blockedA变量,而是已经被封装成了__Block_byref_blockedA_0类型的blockedA,所以在这里传入的是该结构体的地址。也就是说block在这里捕获的是变量的地址 by ref

来看一下现在的__main_block_impl_2也就是NSMallocBlock的结构。

-w586

对应的,还有它的func和desc

-w592

-w510

struct __main_block_desc_2中,多了两个函数指针,分别是copy和dispose。这两个函数与堆block的内存管理有关

-w546

以上图片中的代码,将NSMallocBlock的结构大致地展示出来了。但还有一点需要搞清楚,那就是__main_block_impl_2__block_byref_blocked_0之间的关系。

将注意里集中到block存储的代码块,也就是__main_block_func_2中。

-w588

可以看到,block取到引用的变量是通过__main_block_impl_2 -> blockedA -> __forwarding -> blockedA取到的。也就是说__main_block_impl_2存放着__Block_byref_blockedA_0的地址,其中的__forwarding指针其实是指向自己,也就是它自身存在一个环。

到这里,产生了一个疑问,就是为什么不直接通过__Block_byref_blockedA_0 -> blockedA取到捕获的值,而是要在中间通过一次__forwarding?

首先来验证一下是否__forwarding真的是指向它自己的,因为这看起来似乎有些多此一举。

通过复制struct结构体到我们的代码文件中并使用__brige转换的方式,我们可以获取到底层结构的值。

-w841

-w644

运行之后,我们可以看到成功地转换出了__main_block_impl_2结构体,而它其中的__forwarding指针也的确是指向它自己…

-w385

其实,这是出于内存管理的原因。

当block在栈中时,forwarding指针指向自己,block通过forwarding指针找到捕获的值,这时候直接不通过forwarding指针也是可以的。

但当block被copy到了堆上时,对它捕获的变量(在这里是blockA),也会将__Block_byref_blockedA_0copy一份到堆上。这时如果直接通过blockedA->blockedA修改的仍然是栈上的值。

在block被copy到堆时,__Block_byref_blockedA_0内的forwarding指针会被指向堆上的__Block_byref_blockedA_0结构体,所以通过blockedA -> forwarding -> blockedA这种方式可以保证正确地找到真正的blockedA值。

-w650

综上,NSMallocBlock的结构大致如下图。

-w689

NSStackBlock / NSGlobalBlock

这两类的Block在捕获变量后的变化相对NSMallocBlock来说比较简单

  • 当block不捕获变量时,按源码分析来说它应该是一个stack block,但实测当不捕获变量时stack block会被优化成一个global block。

  • 而当捕获了局部变量(auto 变量)时,block会变成malloc block。然而,我们知道捕获简单的局部变量后,在block内对变量的修改并不能传递出block内部,原因就是block在捕获时是直接捕获了变量的值。

-w585

上图是一个捕获了局部变量a的block。可以看到在结构体内部有一个a属性,在block初始化时直接将a的值捕获进block内部,所以block内部对a的修改并不会传递出block外。事实上,在block内修改局部变量在编译时是会failed的,编译器将提示若要修改需要加上__blcok修饰符

但需要注意的是,以上讨论是基于捕获的变量为基础数据类型。若捕获的变量为对象,则会捕获指针进block结构体中,我们可以通过该对象修改它的属性值,但不可以对对象自身重新赋值。

-w581

  • 静态局部变量

接着讨论一下捕获静态局部变量的情况。
-w547

-w592

可以看到,捕获静态局部变量时是直接捕获变量的指针的,也就是说可以在block内直接修改变量的值。

  • 全局变量

-w581

-w595

可以看到,block内部并没有捕获全局变量,对全局变量的访问和修改是直接进行的。

最后,回顾一下四个block的类别
-w476

其中,block1引用了局部变量,block2引用了全局变量,block3引用了被__block修饰的局部变量,block4引用了静态局部变量。

总结:

  • 不引用变量的block为栈区block,但在实测中这类block被转换为global block
  • 引用局部变量或__block变量的block即为malloc block
  • 引用静态局部变量或全局变量的block为global block

内存管理

局部对象变量

当捕获局部的对象时,若对象为strong类型,则在捕获时会对该对象进行强引用,引用计数+1.

而当block被copy到堆上时,也会copy一份引用的对象,这时该对象的引用计数又会+1,所以最后引用计数从1变成了3.

-w951

静态局部变量

静态局部变量在被block捕获时是直接传递指针,所以block捕获的是对象指针的指针。

-w586

所以在最后,捕获对象的引用计数并不会增加。

全局变量

由于使用全局变量时,block是直接访问不进行捕获,所以也不会强引用对象而改变对象的引用计数,即引用计数仍为1.

__block 对象

在笔者在Xcode中尝试的时候,发现__block修饰的变量被捕获后引用计数也仍为1,但显示该block已经为NSMallocBlock了。这里有一个小疑问:为什么引用计数不增加?

-w628

总结

block的行为会影响到block本身实际的结构体构成以及它在运行时的内存管理策略,更详细的代码需要阅读libclosure库来进一步学习。

CATALOG
  1. 1. Block在Objective-C中的定义与实现
    1. 1.0.1. Block定义
      1. 1.0.1.1. struct __main_block_impl_0
      2. 1.0.1.2. struct __main_block_desc_x
      3. 1.0.1.3. struct __block_impl
    2. 1.0.2. Block分类
    3. 1.0.3. 捕获变量后的block
      1. 1.0.3.1. NSMallocBlock
      2. 1.0.3.2. NSStackBlock / NSGlobalBlock
    4. 1.0.4. 内存管理
      1. 1.0.4.1. 局部对象变量
      2. 1.0.4.2. 静态局部变量
      3. 1.0.4.3. 全局变量
      4. 1.0.4.4. __block 对象
    5. 1.0.5. 总结