Block究竟是什么,我们先从c++代码开始
从一个最简单的block结构开始

clang -rewrite-objc main.m -o main.cpp && open main.cpp


为了方便阅读 我们简化一下代码

为了方便进一步阅读,这里对其中的命名做了简化,参考下面的简单流程

-
结合clang编译中间c++代码,通过block的创建,结合上图, 脑子里先勾勒一个sketch
-
创建两层结构
-
BlockCreate 结构
-
Block结构,属于BlockCreate的成员
-
-
通过BlockCreate构造传参,实例化BlockCreate成员Block::block
-
最终返回的是一个 BlockCreate结构指针
-
通过BlockCreate结构首地址,我们可以拿到成员Block::block, BlockCreate首地址与 成员block首地址是一样的,因为block位于 BlockCreate内存空间的起始处
既然可以拿到首地址(成员block地址),那么同样可以通过内存偏移拿到成员Desc的地址
-
通过拿到成员Block::block地址,就可以调用Block::block的成员方法FuncPtr了, 而FuncPtr恰恰是通过 BlockCreate构造实例化Block::block成员时 赋值的fun函数入口地址
-
- 一定要了解这个两层结构,虽然不是真正意义源码,但是对后面我们分析源码很有帮助
前面的例子没有使用变量,我们可以通过前面的方式再操作一次,对比下区别


当自定义的结构体内部访问外部的一个局部变量时
你会发现clang生成的c++代码发生了变化 对照上面的那个实例化流程
-
BlockCreate 结构体内多了一个成员 int a
-
BlockCreate构造 也多了一个传参
-
func函数内部访问变量 a通过 func(BlockCreate self) 的 BlockCreate::self 来获取拷贝
-
你会发现有3处存在变量a
-
main函数内的局部变量a
-
BlockCrete 结构体内的成员变量a
-
func方法内部的局部变量a
其实这3个变量a分别是3个不同的变量了
-
把局部变量a改为static修饰,继续clang c++查看


用static修饰变量a,不一样了
BlockCreate构造传参,此时传递的是 a的地址,而BlockCreate成员 a也变成了 指针, func内部的局部变量a 也变成了 指针,func内部的a是通过 BlockCreate::*self 的指针a 赋值 给func内部的局部变量 指针a
所以static修饰a后,func内部访问的a其实还是 main函数内部的 指针a
把局部变量a改为 __block修饰,继续clang c++查看


希望你不会觉得懵,这次复杂了些
-
出现了一个结构 __Block_byref_a_0
-
BlockCreate 成员Desc的结构内部多了两个 函数 copy & dispose
这里简单解释下
-
普通的局部变量a 变成了一个结构 __Block_byref_a_0, a是这个结构的成员
-
成员 void *__isa
-
成员 __block_byref_a_0 *__forwarding;
-
成员 int __flags;
-
成员 int __size
-
成员 int a
__isa 从前面的截图可知。是一个_NSConcreteStackBlock 也就是栈block指针
-
在main里声明的__block修饰的局部变量, 地址赋值给了 __forwarding, 值赋给了 Block_byref结构里的成员a,注意这个设定, 虽然成员也叫a,只是起到一个接收值的作用,关键在于__forwarding 拿到了原来的a的指针
先看下__block修饰的a究竟是怎么访问的
image.png
__forwarding 类型 __Block_byref_a_0 *,类似于链表节点,所以也是一个指向 __Block_byref_a_0 结构的指针 至于有什么用,暂存疑,后面源码接着分析
对比着看,其实很明显,不难理解
image.png
block源码 - libclosure-79 查看
源码入口该怎么查看呢,我们先通过汇编看下

既然retainBlock,说明block开辟了空间,进入查看

继续跳转 br x16

目前找到了_Block_copy这样一个符号,然后进入源码查看

你会看到一个结构Block_layout

Block_layout 就是前面通过clang c++代码 分析出的 两层结构BlockCreate成员 Block::block
__block 修饰变量 测试代码放进 block源码进行调试

这段代码是在block源码中测试的

这其实就是依照Block_layout 栈上的空间结构,在堆区创建了一个Block_layout结构
同时 新开辟的Block_layout结构->invoke 从原来栈上Block_layout->invoke拷贝过来

既然是堆上开辟空间创建的Block_layout结构,自然isa 指向 _NSConcreteMallocBlock (堆block)
block分析源码遇到问题
现在还有两块没探索到源码,就是 前面通过clang 编译生成的c++代码中__Block_byref_a_0这样的结构,还有一块是BlockCreate构造逻辑部分
那么接下来该何去何从?
我选择最原始的方式 汇编 + 下符号断点 + 结合clang c++代码分析

先把代码断到此处,防止dyld其他流程干扰

下符号断点 同时把前面分析过的 _Block_copy 符号也下下来,为了方便分析流程
跟着调试 进入 _Block_object_dispose:

回到之前clang编译出的c++代码看下

既然下到了符号_Block_object_dispose 那么同样也把符号 _Block_object_copy下下来继续调试
没有的话 就试试 _Block_object_assign, 之所以没有找到 _Block_object_copy符号,是因为那是由编译器决定的
成功断点符号 _Block_object_assign

找到头绪,自然我们又回到了源码

-
看下源码注释
When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point to do the assignment.
当Blocks(可以理解为前面的有成员func的那个结构) 或者 Block_byref持有对象时候,这个入口就会被触发 执行赋值操作

-
__block int a = 10 类型为 BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK or BLOCK_FIELD_IS_BYREF
执行 _Block_byref_copy()
_Block_byref_copy
在分析_Block_byref_copy流程之前,我们需要了解下Block_byref 是什么

从前面clang编译拿到的c++代码,可以看到,Block_byref 是对常规变量的封装,封装结构里还多了isa,__forwarding成员

源码中还存在 Block_byref_2 Block_byref_3 两个结构,暂且不表,后面会继续说明
我们可以做个假设,目前我们测试的实例 是block引用外部 __block修饰的变量,我们也是这么用的,既然block内部访问外部变量,那么也会对于这个变量的引用计数产生影响 flags
就是存储引用计数的
_Block_byref_copy翻译

如果源byref结构已经在heap上,则不需要执行拷贝,引用计数+1

中间有一段内存偏移的代码,还没解析,继续

byref_keep byref_destroy 究竟实现了什么功能
因为我们用的常规变量a测试 我们换成object看下
将变量a换为object测试

clang c++代码


从源码得知



131有什么意义

两个参数 + 40 什么意思

按照编译的逻辑,byref_keep 就是 object类型的对象的 拷贝
但是运行时会做修正 流程有差别
同样 byref_destroy:

以上为 Block_byref 逻辑,再通过clang得到的c++ 看下 Block_layout 的处理


再确认下 __block修饰的 object对象,在block体里 究竟是如何访问的

总结
-
__block 修饰变量之后,编译器会在栈上构建一个 栈Block_byref(包含变量指针)
-
定义block,可以理解为编译器生成一个中间结构BlockCreate(这个名字是特意起的,知道是个结构,为了便于理解,你可以这么理解)
- 同时编译器会在栈上初始化构建一个 栈Block_layout(包含func成员)
-
执行BlockCreate构造方法
-
通过Block_layout首地址偏移 得到 Block_copy函数地址, 执行Block_copy,把 栈Block_byref 拷贝 到堆Block_byref
-
构造参数 栈Block_byref,通过Block_byref首地址偏移 得到 Block_byref_2(包含_Block_byref_copy 即byref拷贝函数)首地址, 执行 _Block_byref_copy函数, 把栈Block_byref 拷贝到 堆Block_byref
-
继续上一步的位置 内存偏移 8字节,得到堆上开辟的 object内存空间首地址, 这里当然就存放 object对象了
-
需要注意的一个细节
栈Block_byref 拷贝到 堆Block_byref之后,由于堆上是新的内存空间,那么栈与堆不就两个空间了吗,如何保障访问的是同一块内存?就觉办法就是,在拷贝之后, 把 栈Block_byref 和 堆Block_byref 里的forwarding 都指向了 堆Block_byref, 也就是 堆 forwarding再指向一遍自己
__block修饰变量之后,不管是在block块内访问变量,还是在block外访问变量,都是通过 forwarding访问到堆空间,然后再访问目标空间内的 变量, 这样就保障了 访问的变量是同一块内存空间
-
image.png
image.png

-
Block_byref 持有的变量生命周期结束,执行 _Block_object_dispose
- 执行_Block_byref_release函数,根据Block_byref 首地址偏移 找到 Block_byref_2首地址,继续偏移8字节 得到 byref_destroy 执行析构 回收堆内存空间
-
Block_layout 作用域结束 或者 生命周期结束, 执行 _Block_release
- 根据 Block_layout 首地址偏移 找到 Block_descriptor_2 首地址,继续偏移8字节,的到 dispose 执行析构 回收 堆上开辟的 Block_layout 堆内存空间
读取寄存器查看

符号断点 _Block_copy

_Block_copy 执行之前,寄存器rax接收参数 (arm64 读取寄存器x1)

执行完之后,ret返回,rax寄存器存储返回值

- 变量a 改为 __block修饰

因为__block修饰,Block_layout 中就出现了 copy 函数地址,通过copy,就执行 _Block_copy
而没有__block修饰,没有 copy dispose 函数,默认执行 _Block_copy
造成这个差别就在于 构造传参时候 flag的区别,__block修饰之前 是0,__block修饰之后, 1 << 25

网友评论