探秘block原理
01
概述
在iOS
开发中,block
大家用的都很熟悉了,是iOS
开发中闭包的一种实现方式,可以对一段代码逻辑进行封装,使其可以像数据一样被传递、存储、调用,并且可以保存相关的上下文状态。
很多block
原理性的文章都比较老,里面讲的一些知识已经过时,这里用新版的iOS SDK
再梳理一遍block
原理,也是和大家一起对已有知识做一次复习。
02
内存布局
block
本质上可以理解为结构体,对于结构体的内存布局,先用一张图来表示一下,图中字段顺序按照布局的先后顺序:
isa:
block
也有isa
,从内存结构上也属于对象,isa
指向的是block
的类对象,类对象例如__NSMallocBlock__
,后续文章会讲到;flags:用于存储一些标志位信息,例如是否捕获外部变量;
reserved:系统保留字段,后续可能会用于一些编译优化标志位,或者存储一些临时变量的处理;
invoke:函数指针,指向了
block
要执行的函数地址,也就是block
代码块对应的函数地址;descriptor(现在叫desc):指向
block_desc_0
,包含block
大小、捕获的外部变量布局信息、增加引用计数和销毁的相关函数指针;variables:
block
捕获的外部变量。

03
类型
由于block
也是对象,可以通过class
方法获取到其类型,也就是类对象。block
有下面三种类型:
__NSGlobalBlock__
,没有访问auto
变量的block
,访问static
变量是没问题的。这种类型的变量并没有什么意义,如果不需要用到auto
变量,写成方法就可以满足需求;__NSStackBlock__
,在MRC
环境下,访问了auto
变量,会默认被放在栈区。需要手动copy
到堆区,ARC
环境下会在访问auto
变量后,会自动拷贝到堆区;__NSMallocBlock__
,由开发者自己管理内存,不会由系统来释放。
block
的分配主要是在三个区域,堆区、栈区、全局区,全局区的数据存储在数据段。
block
在不同的场景会存在不同的内存区域中,在MRC
中创建一个block
首先是在__NSStackBlock__
内存中的,然后我们使用copy
方法将block
拷贝到__NSMallocBlock__
内存中进行内存管理。后来在ARC
中系统已经帮我们做好了copy
的操作,创建的block
会自动copy
到__NSMallocBlock__
内存中,堆区的block
也有引用计数的概念。如果这个block
中没有用到任何外部参数,系统会将这个block
存放在__NSGlobalBlock__
内存中。

并且block
也有继承关系,以下面TestBlock
的实例来说,其父类是__NSGlobalBlock__
,所有block
的父类是NSBlock
,并且NSBlock
继承自NSObject
类。在更早一些的iOS
系统中,__NSGlobalBlock__
和NSBlock
之间,还会有一层__NSGlobalBlock
的关系(后面没有下划线)。

04
转换C++
下面,我们通过clang
命令将block
转为结构体,来分析下其具体实现。虽然这并不是最终运行在iOS
系统上的代码,其等于一种中间表现形式,后续编译链接优化才会形成运行在手机上的ipa
包,但对于我们了解block
的实现原理有很大帮助。
4.1转换命令
xcrun
是Xcode
用于查找和执行相关命令行的工具集,可以更好的执行clang
命令,减少报错。
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc [源文件路径] -o [目标文件路径]
clang
命令有下面这些关键参数:
-fobjc-arc:如果项目是
ARC
或者ARC
和MRC
混编的环境,需要通过此参数修饰,表示按ARC
的方式进行转换,如果不需要ARC
环境可以忽略;-x objective-c++:此参数上面没用,如果包含
Objective++
源文件的时候,需要用到此参数,以确保clang
可以区分OC
和C++
代码;-rewrite-objc:告诉
clang
以C++
的方式重写出来,包含的上层代码,clang
会以底层代码的方式进行展现;[目标文件路径]:非必传参数,不传的话默认在当前目录生成一个同名的
cpp
文件,例如main.m
对应main.cpp
。
4.2转换示例
下面在main.m
中实现了一个很简单的block
,并且没有捕获任何外部变量,通过clang
命令查看C++
代码,观察block
的具体实现原理。

转换后将C++
源文件拉到最下面,可以看到main
函数以及TestBlock
的实现,main
函数中有很多转义代码,删掉后梳理逻辑会更清晰。

05
结构体
5.1基础结构
转换后的代码看着比较复杂,但我们只看关键信息,__main_block_impl_0
构造函数也可以去掉,整理后就是下面三个结构体。在不包含外部变量和__block
的前提下,block
结构体各个字段就这么简单,关键就是isa
、Block_size
、FuncPtr
这三个。

我们也可以打印block
结构体相关字段,但由于block
的结构体并没有声明在某个.h
文件中,所以需要我们讲clang
转换后的结构体粘到对应的文件中,做显示声明。随后用__bridge
的方式,将block
对象桥接为自己声明的结构体,即可打印对应字段。

结构体中impl.FuncPtr
存储的就是回调函数地址,从地址可以看出是一个虚拟地址,block
结构体都存储在堆区。

5.2调用部分
看完block
结构体的定义,我们来到main
函数中,看block
的实现和调用转换后是什么样的。将main
函数中block
相关的转换都去掉,结果如红圈部分。本质上就是两步,第一步是调用__main_block_impl_0
的结构体构造函数,第二步是调用结构体的函数指针。

第一行main
函数中调用的构造方法,是__main_block_impl_0
结构体声明的C++
构造函数,因为我们创建的是一个最简单block
,可以看到block
的存储区域是在stack
栈区的。即main
函数调用完,block
生命周期就会结束。

__main_block_impl_0
构造函数有两个参数,第一个红圈部分就是传入函数指针地址,函数对应的就是block
内部的实现代码。第二个参数是__main_block_desc_0_DATA
结构体,其定义为__main_block_desc_0
,并且默认实现第一个参数传0
,第二个参数是block
结构体的大小,结构体为__main_block_impl_0 block
自身的结构体大小。第三个参数有默认值,可以不传。

__main_block_desc_0
结构体是一种紧凑型的写法,在声明__main_block_desc_0
结构体后,紧接着声明了一个名为__main_block_desc_0_DATA
的变量,变量类型为静态变量,并且实现了初始化相关代码。

在执行block
的代码位置,可以看到并不是block->impl.FuncPtr
的方式调用,而是直接block->FuncPtr
的方式调用,中间少了一步。
严谨些来说应该加上impl
,但不加也不会出问题。这是因为,如果看未删除转换代码的原始clang
代码,可以看到block
是被转换为__block_impl
的,也就是说被当做__block_impl
看待的。如果再结合__main_block_impl_0
的结构体定义来看,__block_impl
在成员变量的第一位,所以访问FuncPtr
是没有问题的,只要不访问Desc
就是可以的。
06
外部变量
6.1值类型
如果在block
的调用中加一个外部变量,那结构体将会是怎样的?

通过clang
命令可以可以看到,转换后的__main_block_impl_0
中增加了一个同名字段,这很简单没必要过多解释。在__main_block_impl_0
构造函数中传入,通过冒号后的初始化列表对value
参数进行初始化。

后面传参和使用,就都是结构体赋值和取值逻辑,很简单。

6.2值传递
下面这种写法,在block
的使用中很容易踩坑。在block
中使用value
参数,并且打印value
参数,发现结果为1
,而不是2
。

通过C++
源码我们可以看到,这是因为如果block
引用的外部变量是值类型,会采取直接复制值的方式,而不是指针引用。

想解决这个问题也很简单,通过__block
修饰一下值类型,即可实现block
内value
的值和外部value
参数统一。

6.3静态变量
我们看一下,如果捕获的是一个static
修饰的静态变量,其结构体会是什么实现。

转换为C++
代码后,可以看到原来的值传递变成了地址传递,__main_block_impl_0
中value
的引用是指针引用,在main
函数中将value
的地址传入。如果被static
修饰的本身就是一个对象,对象是通过指针引用的,在block
的结构体中就是两个星号引用。也就是NSObject **obj
。

正是由于静态变量地址传递的实现,在block
内可以对静态变量直接进行更改,而无需用__block
进行修饰。

6.4全局变量
如果把value
改为全局变量,结构体会有什么变化呢?

因为全局变量的作用域很大,所以并不需要block
进行单独持有即可访问,结构体并不会新增字段。

6.5对象类型变量
如果block
中引用的是对象,而不是基础数据类型,结构体会是什么定义呢?

执行clang
命令,执行完成后结构体是下图的,下面代码去掉了转换,以及整理过代码。可以看到多了两个函数指针,__main_block_copy_0
和__main_block_dispose_0
。
以copy
的实现__main_block_copy_0
为例,执行后会调用Block_object_assign
的实现,在实现中系统会根据person
的引用方式,__strong
、__weak
、__unsafe_unretained
,是强引用还是弱引用,调用对应的内存管理方法。
__main_block_dispose_0
函数在block
从堆区移除的时候被调用,调用dispose
时会调用实现Block_object_dispose
函数,函数中会根据person
的引用方式,进行对应的减少引用计数或释放操作。
copy
和dispose
两个函数都有一个3
的参数,这个参数是一个标志位,表示外部变量类型。这里是BLOCK_FIELD_IS_OBJECT
表示一个对象类型,也有BLOCK_FIELD_IS_WEAK
表示weak
引用的变量,BLOCK_FIELD_IS_BLOCK
表示block
类型的变量等。

07
结尾
感谢大家能把文章读完,这篇文章并不会包含__block
和__weak
相关知识,为了更系统的了解这两部分,后面会新出一篇文章整体来讲一下,敬请期待~