Objectvie-C Block
block 在 Objective-C开发中应用非常广泛,我们知道block会捕获外部对象,也知道使用block要防止循环引用。
“知其然不知所以然“是一件非常痛苦的。那么block这套机制在OC中是如何实现的呢?本文通过从C/C++到汇编层面分析block的实现原理。
Clang
clang 是Xcode的编译器前端,编译器前端负责语法分析、语义分析、生成中间代码。
比如当你再xcode中进行build一个.m 文件的时候,实际的编译命令如下:
|
|
Objective-C 也可以用GCC来编译,本文不做讲解。
clang除了能够进行编译以外,还有其他的一些用法。比如本文分析代码的核心命令就是这个:
通过这个命令,我们可以把Objective-C的代码用C++表示.
对于想深入理解clang命令同学,可以用明令行自带的工具来查看帮助文档
或者阅读clang官方文档.
查看汇编代码
在Xcode中,对于一个源文件,我们可以通过如下方式查看其汇编代码。这对于我们分析代码深层次的实现原理非常有用,这个在后面也可以看到。

Objective-C对象内存模型
为了本文讲解的更清楚,我们首先来看一个Objective-C对象的内存模型。我们首先常见一个类内容如下
TestClass.h
TestClass.m
然后,我们用上文提到的clang命令将TestClass.m 转换成C++的表示
当转换完毕当前目录会多一个TestClass.cpp文件,这个文件很大,接近十万行。
我们先搜索testFunction方法,以此方法作为切入
可以看到,转换成C++后,一个实例方法转换为一个静态方法,这个方法的内容看起来很乱,因为各种类型强制转换,去掉后就比较清楚了。
可以看到:
- 转换后增加了两个参数: self 和 _cmd
- 方法的调用换成了objc_msgSend,这是一个C函数,两个参数分别是Class 和 SEL
到这里,我们知道了一个OC的实例方法具体是怎么实现的了。
那么,一个OC对象在内存中是如何存储的呢?我们在刚刚的方法上可以找到这个类的完整实现:
我们侧重来看看类对应的结构体
可以看到,OC类实际上是按照以下方式存储对象的
- isa指针。 指向objc_class 类型的结构体,这个结构体存储了方法的列表等类相关的信息,因为objc_msgSend中,发给对象的实际是一个字符串,运行时就是通过isa找到类对象,然后通过字符串找到方法的实际执行的。
- ivar .属性背后的存储对象,到这里也可以看出来一个普通的属性就是ivar+getter+setter。
也就是说,只要有isa指针,指向一个类对象,那么这个结构就能处理OC的消息机制,也就能当成OC的对象来用。
Block的本质
我们修改TestClass.m文件中的内容如下:
然后,重新用clang转换为C++,有关代码的内容如下:
我们还是以方法作为切入点,看看具体是怎么实现的。_I_TestClass_testFunction是testFunction 转换后的方法。我们去掉一些强制代码,这样看起来更清楚:
- 初始化一个variable(也就是block捕获的变量)
- 调用结构体testClasstestFunction_block_impl_0的构造函数来新建一个结构体,并且把地址赋值给temp变量(也就是初始化一个block)
- 通过调用temp变量内的函数指针(C的函数指针)来执行实际的函数。
通过分析,我们大概能知道Block的实现
block背后的内存模型实际上是一个结构体,这个结构体会存储一个函数指针来指向block的实际执行代码。
接着,我们来深入的研究下block背后的结构体,也就是这个结构体TestClasstestFunction_block_impl_0:
我们回头看看block初始化那句代码
在对应之前代码块的构造函数,我们可以清楚的看到,在初始化的时候三个参数一次是
- 函数指针 TestClasstestFunction_block_func_0
- block的描述结构体(全局静态结构体)TestClasstestFunction_block_desc_0_DATA
- 捕获的变量variable
接着,我们看看block背后的C函数TestClasstestFunction_block_func_0
Tips:
内存中存储区域可分为几个区域:
- TEXT 代码区
- DATA 数据区
- Stack 栈区
- HEAP 堆区
上文的字符串@”%ld”,对应C++代码是)&NSConstantStringImplvar_folders_c9_vtczmvz10kq_lgl88b4xvhs80000gn_T_TestClassd8c269
mi_0,是存储在数据区的。这样即使程序中有多个@”%ld”,也不会创建多个实例。
可以看到。这个C函数的参数
TestClasstestFunction_block_impl_0,也就是一个block类型。然后在方法体内部,使用这个block 参数。
最后,我们分析block的描述信息,也就是这段代码
这段代码不难理解,就是声明一个描述信息的结构体,然后初始化这个结构体的全局静态变量。
分析到这里,上面代码的大多数内容我们都理解了,但是有一点我们还没有搞清楚,就是isa指向的内容_NSConcreteStackBlock
但是,到这里我们知道了为什么Block可以当作OC对象来用的原因:就是这个指向类对象的isa指针。
Block 类型
上文提到了_NSConcreteStackBlock是Block一种,block一共有三种类型
- NSConcreteStackBlock 栈上分配,作用域结束后自动释放
- NSConcreteGlobalBlock 全局分配,类似全局变量,存储在数据段,内存中只有一份
- NSConcreteHeapBlock 堆上分配
我们仍然尝试用Clang转换的方式,来验证我们的理论。将TestClass.m内容修修改为
|
|
然后转成C++之后,分别对应如下
全局globalBlock
栈上stackBlock
属性Block
What the fuck! 怎么属性的block是栈类型的,难道不该是堆类型的吗?
到这里,C/C++层面的代码已经无法满足我们的需求了。我们试着把代码转成汇编,一探究竟:
方便分析属性block究竟是怎么回事,我们修改.m文件
转换成汇编后,在方法部分我们看到类似汇编代码
|
|
也就是说,在返回方法之前,一次调用了
那么 _objc_retainBlock 就是block从栈到堆得黑魔法。
我们通过runtime的源码来分析这个方法的实现:
到这里我们就清楚了,编译器为我们自动插入了 _objc_retainBlock ,而这个函数就是把栈上的block拷贝到堆上。
Tips: 通常在写属性的时候,block都会声明为copy。这是显式的表示,即使block是栈上的,也会拷贝到堆上。其实在赋值的时候,编译器已经自动帮我们做了这些,所以其实使用strong也可以。
那么,一个临时变量的block会被拷贝到堆上么?
修改testFunction :
继续查看汇编:
我们仍然看到了 _objc_retainBlock ,也就是即使一个函数中的block,在ARC开启的情况下,仍然会拷贝到堆上。
__block
通过之前的研究,我们知道了block如何捕获外部变量,也知道了block的几种类型。那么block如何修改外部变量的呢?
block 是不可以直接修改外部变量的,比如:
直接那么写,编译器是不会通过的,想想也简单,因为变量可能在block之前就被释放掉了,直接那么赋值会导致野指针。
在OC层面,我们可以通过__block关键字,那么加了这个关键字之后,实际上C++层面的代码时什么样的呢?
在转换成C++代码后,如下:
可以看到,__block NSInteger variable = 0转换成了一个结构体
|
|
这个结构体的定义如下:
通过初始化我们可以看到
- __isa指向0
- forwarding 指向Block_byref_variable_0自身
- __flags为0
- __size就是结构题的大小
variable是我们定义的原始值0
到这里,我们有一点疑惑为什么要存在一个__forwarding来指向自身呢?
我们来看看block的方法体,也就是这部分123^{variable = 1;}
转换成C++后:
也就是说__forwarding存在的意义就是通过它来访问到变量的地址,如果这个指针一直指向自身,那么它也就没有存在的意义,也就是在将来的某一个时间点,它一定会指向另外一个数据结构。
我们在上文中讲到,ARC开启的时候,栈上的block会被复制到堆上。
在没有复制之前

复制之后

这样,我们就清楚原因了:
即使发生了复制,只要修改__forwarding的指向,我们就能够保证栈上和堆上的block都访问同一个对象。
Block 对象捕获
到这里,我们分析的block都是捕获一个外部值,并不是对象。值和对象最大的区别就是对象有生命周期,对象我们需要考虑引用计数。
修改testFunction:
再转换成C++后,我们对比之前捕获NSInteger,发现多了两个生命周期管理函数
|
|
我们再查看下Block_object_assign和Block_object_dispose的定义
也就是说,编译器通过这两个函数来管理Block捕获对象的生命周期。其中
- _Block_object_assign相当于ARC中的reatain,在block从栈上拷贝到堆上的时候调用
- _Block_object_dispose相当于ARC中的release,在block堆上废弃的时候调用
总结
- block在C语言层面就是结构体,结构体存储了函数指针和捕获的变量列表
- block分为全局,栈上,堆上三种,ARC开启的时候,会自动把栈上的block拷贝到堆上
- __block变量在C语言层面也是一个结构体
- block捕获对象的时候会增加对象的引用计数。