- clang工具
block分类- block 结构
- block调用
- block类型以及ARC对block的影响
- 外部变量对block的影响
参考文章:
Block技巧与底层解析
Block底层实现分析
iOS Block底层探索
Block-ABI-Apple
clang工具
clang结构化编译器前端,简单理解为可以编译llvm架构的代码工具
Clang 对源程序进行词法分析和语义分析,并将分析结果转换为 Abstract Syntax Tree ( 抽象语法树 ) ,最后使用 LLVM 作为后端代码的生成器。
使用方法:clang -rewrite-objc 文件名
新建一个工程,执行clang -rewrite-objc main.c会生成一个main.cpp文件

block 结构
先看一个简单的block:
clang 之后看一下main.cpp, 把多余代码删掉主要看以下代码:
通过对比可以看到,block对应struct __main_block_impl_0 这个结构体,意思就是main函数中第0个block实现,这个结构体包含
struct __block_impl implvoid *isa- 指向对应类型的指针
int Flags- 标志变量,在实现block的内部操作时会用到
int Reserved- 保留字段
void *FuncPtrblock执行时调用的函数的指针
struct __main_block_desc_0size_t reserved- 保留字段
size_t Block_sizeblock大小
__main_block_impl_0- 显式的构造函数
这里有一个纠结的地方block到底是__main_block_impl_0 还是__block_impl,目前理解为__block_impl为系统定义block的实现,__main_block_impl_0是实际block实现,相当于在block本质实现的基础上新增了特性。
对比官方定义的block
|
|
其中invoke和FuncPtr是一样的只是clang生成的变量名不同,copy和dispose时捕获外部变量时使用,在下面会讨论。
所以得出一个结论block是一个包含调用函数指针、block外部上下文变量的结构体,其次内部包含isa指针,说明block也是一个对象
block调用
创建简单block
执行clang,其他生成代码都和上面基本一致主要看main函数
- 调用
__main_block_impl_2显式构造函数 &将1结果地址赋值给testblock- 将
testblock强转成__block_impl调用FuncPtr也就是__main_block_func_2
这里有一个问题,理论上testblock的类型是__main_block_impl_2为什么可以强转成__block_impl?
这是因为&取得是起始地址,结构体的起始地址和他第一个元素的起始地址是一致的也就是说&__main_block_impl_2 和 &(__main_block_impl_2->__block_impl)地址是一样的,所以这里可以强制转化
block类型以及ARC对block的影响
block的常见类型有3种:
NSConcreteStackBlock(栈)NSConcreteGlobalBlock(全局)NSConcreteMallocBlock(堆)
我们先简单创建两个block
对其进行编译转换后得到以下缩略代码:
观察一下上面简单block发现impl.isa便是对应的block类型;可以看到globalBlock属于NSConcreteGlobalBlock,stackBlock属于NSConcreteStackBlock。
然而我们实际输出:

stackblock也属于globalBlock,why???
参照唐巧博客解释
由于 clang 改写的具体实现方式和 LLVM 不太一样,并且这里没有开启 ARC。所以这里我们看到 isa 指向的还是_NSConcreteStackBlock。但在 LLVM 的实现中,开启 ARC 时,block 应该是 _NSConcreteGlobalBlock 类型
详细的LLVM解析看llvm对于Block的编译规则(我没全部看完😅)。
可以理解为由于block中的代码没有捕获任何外部变量,这个block不存在任何内存泄漏的风险,也不需要引用计数,所以类型为__NSGlobalBlock__。
所以如果block内部引用了外部变量就不会变成__NSGlobalBlock__,新增以下代码:
clang 之后:
发现impl.isa指向NSConcreteStackBlock,然而我们输出发现:

创建block时是在__NSStackBlock__,而赋值给blockWithVar后,blockWithVar属于__NSMallocBlock__,这是因为ARC环境下
在 ARC 下,block 类型通过=进行传递时,会导致调用objc_retainBlock->_Block_copy->_Block_copy_internal方法链。并导致 NSStackBlock 类型的 block 转换为 NSMallocBlock 类型。
原文地址:https://www.jianshu.com/p/0855b68d1c1d
NSObject.mm源代码可以看到
|
|
_Block_copy是在runtime.c中实现的
关闭ARC测试下:

此时输出blockWithVar是属于__NSStackBlock__

我们打开ARC继续看,ARC环境下所有的block赋值给变量都会copy到堆上吗?

发现使用__weak修饰时并不会复制到堆上。所以如果使用要注意!!!
ARC对类型为strong且捕获了外部变量的block进行了copy。并且当block类型为strong,但是创建时没有捕获外部变量,block最终会变成NSGlobalBlock类型
外部变量对block的影响
捕捉局部变量的影响
首先看下面的代码
转化后
|
|
对比发现blockWithVar在转化后多了一个int a的变量,同时在显式构造函数里多了int _a,后面的: a(_a)相当于a = _a,是c++中的初始化列表。通过
发现a传递到block中是值传递,在调用里会生成另外一个a(int a = __cself->a;) 所以我们在block中更改a的值是不会生效的。同时编译器也会报错

看报错提示加上__block,那我们加上__block看会有什么影响:
转化后关键代码:
- 先看最后的
_I_TestBlock_testStackBlock,发现加上__block关键字之后a已经不是int类型而是对应__Block_byref_a_0类型 - 再观察
__Block_byref_a_0包含:void *__isa说明是一个对象__Block_byref_a_0 *__forwarding;int __flags;int __size;int a;这里面的a对应的就是block外面赋的值
- 继续观察
(__Block_byref_a_0 *)&a,在block编译时将(__Block_byref_a_0 *)&a传给了block,所以不再是值传递而是内存地址传递,所以在block内可以操纵a
那么如果直接传递内存地址而不使用__block可以吗?
将代码修改如下
运行发现可以修改,但这样很明显可以看出来如果a释放了,p就变成了野指针,如果block是作为参数或者返回值,这些类型都是跨栈的,也就是说再次调用会造成野指针错误。例如下面的代码:
捕捉局部静态变量的影响
转化后
可以看到此时a是地址传递,在block内部也可以成功更改a的值,
需要注意一点的是静态局部变量是存储在静态数据存储区域的,也就是和程序拥有一样的生命周期,也就是说在程序运行时,都能够保证block访问到一个有效的变量。但是其作用范围还是局限于定义它的函数中,所以只能在block通过静态局部变量的地址来进行访问。
捕捉全局变量的影响
转化后
可以看到全局变量都是直接访问变量的,是因为全局变量存储在静态数据存储区,在程序结束前不会被销毁
实例变量
这里编译器会给我们警告,意思就是有隐式的self引用,我们转化一下
通过上面看到block内部会生成一个TestBlock *self,它的值便是_I_TestBlock_testStackBlock中的self所以可以更改实例变量,当然这里会有一个循环引用的问题,也就是说block引用实例变量也会强引用self
总结:
block本质上是一个包含调用函数指针、block外部上下文变量的结构体block有根据存储位置不同分为三种类型- NSConcreteStackBlock(栈)NSConcreteGlobalBlock(全局)NSConcreteMallocBlock(堆)
ARC模式下会把不引用外部变量的block转化成NSConcreteGlobalBlock,引用外部变量的block会在赋值时转化为NSConcreteMallocBlockblock引用外部局部变量和静态局部变量或实例变量时会在block内部生成对应的变量。在引用全局变量时并不会生成对应变量。Block会对内部的变量形成强引用,而如果同时该变量又持有这个Block,就会导致循环引用而无法释放,从而导致内存泄露。注意隐式的循环引用