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究竟是怎么访问的__forwarding 范例 __Block_byref_a_0 *,雷同于链表节点,以是也是一个指向 __Block_byref_a_0 布局的指针 至于有什么用,暂存疑,后面源码接着分析对比着看,着实很显着,不难明白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访问到堆空间,然后再访问目的空间内的 变量, 这样就保障了 访问的变量是同一块内存空间
- 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寄存器存储返回值
由于__block修饰,Block_layout 中就出现了 copy 函数地点,通过copy,就实行 _Block_copy
而没有__block修饰,没有 copy dispose 函数,默认实行 _Block_copy
造成这个差异就在于 构造传参时间 flag的区别,__block修饰之前 是0,__block修饰之后, 1 << 25
|