回顾
在上一篇博客中,通过对block
追根溯源,汇编跟踪调式
,源码分析,对底层结构和 block
的属性方法都有一定的认识, 那么本篇博客将继续对block
的底层进行分析。
iOS底层探索之Block(一)——初识Block(你知道几种Block呢?)
iOS底层探索之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
内部和外部都指向同一个对象
Block_byref_2
- Block_byref_2
这里就是对block
捕获的变量进行处理了,调用了byref_keep
对其生命周期进行保存。Block_byref
的设计思路和Block_layout
中descriptor
流程类似,通过byref->flag
标识码判断对应的属性,以此来判断Block_byref_2
是否存在,如下图所示:
如果我们的block
捕获了使用__block
修饰了外部变量,在cpp
文件中,Block_byref
结构体中就会默认生成两个方法,即对应Block_byref_2
的keep
方法和destory
方法,验证如下:
在
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
的三层拷贝
更多内容持续更新
喜欢就点个赞吧
觉得有收获的,可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我
欢迎大家留言交流,批评指正,互相学习,提升自我