回顾
在上一篇博客中,通过对block追根溯源,汇编跟踪调式,源码分析,对底层结构和 block的属性方法都有一定的认识, 那么本篇博客将继续对block的底层进行分析。
iOS底层探索之Block(一)——初识Block(你知道几种Block呢?)
iOS底层探索之Block(二)——如何解决Block循环引用问题?
iOS底层探索之Block(四)——Block的探索和源码分析
1. block底层探索
block的结构和签名都分析完了,但是block最难的点还是怎么捕获到变量等。
cpp查看底层结构
再去瞄一眼底层结构,如下代码:
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSObject *objc = [NSObject alloc];
void (^jp_block)(void) = ^{
NSLog(@"zjpreno: %@ ",objc);
};
jp_block();
}
@end
通过 xcrun -sdk iphonesimulator clang -S -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.2 ViewController.m 命令生成.cpp文件
通过生成的
.cpp文件可以看到
-
__ViewController__viewDidLoad_block_copy_0是对应Block_descriptor_2里面的copy -
__ViewController__viewDidLoad_block_dispose_0是对应Block_descriptor_2里面的dispose - 在
__ViewController__viewDidLoad_block_copy_0方法里面怎么还多了_Block_object_assign这个玩意,__ViewController__viewDidLoad_block_dispose_0里面多了_Block_object_dispose,让人百思不得其解,先别急请继续往下看👇。
底层结构分析
这个
cpp的结构体即对应源码中的Block_descriptor信息。
-
reserved和size对应Block_descriptor_1的两个属性。 -
void (*copy)和void (*dispose)对应Block_descriptor_2的两个方法。 - 在
copy方法的实现中,会调用_Block_object_assign,此过程即为外部变量的捕获的方法。 - 在
dispose方法的实现中,会调用_Block_object_dispose,此过程为释放方法。
2. block源码分析
在源码中搜索_Block_object_assign,找到如下注释信息:
当
Block 被复制到堆时,一个Block 可以引用四种不同的需要帮助的东西。
- 基于
C++堆栈的对象 - 对
Objective-C对象的引用 - 其他块
-
__block变量
在这些情况下,辅助函数由编译器合成,用于
Block_copy和Block_release,称为复制和处置辅助函数。 复制助手为基于C++堆栈的对象发出对C++ const复制构造函数的调用,并为其余调用运行时支持函数_Block_object_assign。dispose helper调用C++析构函数用于情况 1,调用_Block_object_dispose用于其余情况。
- 编译器在生成复制/处置助手时使用的运行时支持函数
-
_Block_object_assign()和_Block_object_dispose()参数的值
_Block_object_assign 和 _Block_object_dispose 的 flags 参数设置为:
-
BLOCK_FIELD_IS_OBJECT (3),对于Objective-C Object的情况 -
BLOCK_FIELD_IS_BLOCK (7),对于另一个block的情况 -
BLOCK_FIELD_IS_BYREF (8),对于__block变量的情况。
如果 __block 变量被标记为weak,则编译器也在 BLOCK_FIELD_IS_WEAK (16) 中
所以
Block copy/dispose helper应该只生成3、7、8和24四个标志值。
上面是源码的注释,那么现在去验证一下:
从
cpp我们也可以看到对于 OC 对象,是 BLOCK_FIELD_IS_OBJECT (3),那么我们现在去加一下__block,看看会不会变成 BLOCK_FIELD_IS_BYREF (8)呢?看到没有,加了
__block之后就变成 BLOCK_FIELD_IS_BYREF (8),还有谁,45 度仰望天花板,我这该死无处安放的魅力啊!_Block_object_assign
_Block_object_assign
- 如果持有变量是
BLOCK_FIELD_IS_OBJECT类型,即没有__block修饰,dest指针指向objec,引用计数加1,原本的对象地址给了block里面的目标对象,这样就都指向同一个地址空间 - 如果是
BLOCK_FIELD_IS_BLOCK类型,也就是block的类型,就是捕获到了block则进行_Block_copy操作,把objec的内容复制一份到自己的里面,也就是拷贝到堆区 - 如果是
BLOCK_FIELD_IS_BYREF,即有__block修饰,则会调用_Block_byref_copy
_Block_byref_copy
- _Block_byref_copy
将外部对象封装成结构体
Block_byref *src如果是有引用计数的正常对象,就对引用计数进行处理,调用
malloc,生成一个Block_byref *copy,如果不是,则会调用memmove-
设置
forwarding,保证block内部和外部都指向同一个对象
forwarding
Block_byref_2
- Block_byref_2
Block_byref_2
这里就是对block捕获的变量进行处理了,调用了byref_keep对其生命周期进行保存。Block_byref的设计思路和Block_layout中descriptor流程类似,通过byref->flag标识码判断对应的属性,以此来判断Block_byref_2是否存在,如下图所示:
Block_byref
如果我们的block捕获了使用__block修饰了外部变量,在cpp文件中,Block_byref结构体中就会默认生成两个方法,即对应Block_byref_2的keep方法和destory方法,验证如下:
cpp 验证Block_byref_2
在
cpp文件中这两个函数的实现如下:
- _Block_object_assign
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
(char*)dst + 40)相当于 objec,指针平移
上面的 40 是怎么来的呢?如下:
__Block_byref_id_object_copy_131方法的调用会调用_Block_object_assign函数,对Block_byref结构体中的对象进行BLOCK_FIELD_IS_OBJECT流程处理。
_Block_object_dispose
对于_Block_object_dispose方法,也就是释放流程,也是类似的。
也是对
block类型的判断,再调用_Block_release(object)走释放流程。
- flags parameter
3. 总结
block 的三层拷贝
- 如果是
__Block修饰的变量,会对block进行copy操作,从栈区拷贝到堆区 -
block捕获变量,对Block_byref结构体的拷贝 -
Block_byref会对传入的objec进行拷贝,到此完成block的三层拷贝
更多内容持续更新
🌹 喜欢就点个赞吧👍🌹
🌹 觉得有收获的,可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我😁🌹
🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹