本文为L_Ares个人写作,以任何形式转载请表明原文出处。
准备
1. libclosure源码
2. block的.cpp文件
clang获取存在block的.cpp文件的方法在上一篇文中有专门的教程。
一、关于全局Block
前两节中,我们已经了解了常用的Block的3种常见的分类,也知道了全局Block如果捕获了外界变量的话,就会从全局Block变成栈Block。
其实对于Block的类型判断是由编译器进行分辨的,在编译期的时候 :
- 如果
Block并未进行捕获外界变量的操作,那么Block就会被认为是NSGlobalBlock。- 对于带有参数的
Block,参数不属于外界变量,属于Block的内部变量,所以类型也是NSGlobalBlock。- 当
Block对外界变量进行了捕获之后,编译器会对Block的类型判断变成NSStackBlock。- 对
Block是否是全局变量的判断,会存储在Block的结构体属性flags中,在其第28位上。- 编译器不会对
NSGlobalBlock主动做copy操作,即使开发者手动对NSGlobalBlock进行copy方法的调用,NSGlobalBlock也不会发生类型的改变。
二、栈Block变成堆Block的源码解析
在上面,我们已经知道了,全局Block也就是NSGlobalBlock和堆、栈的Block是的区分条件手段 :
编译器的在编译期就通过对
Block是否捕获外界变量进行区分。
而对于捕获外界变量的栈和堆Block,在上一节的Block内存变化中,已经通过汇编的分析,知道了一个条件 :
在声明
Block的时候,会通过objc_retainBlock的_Block_copy将NSStackBlock变成NSMallocBlock。
那么这里就会通过libclosure源码探索一下_Block_copy的实现思路。
操作 :
在准备好的
libclosure源码中全局搜索_Block_copy,找到其在runtime.cpp文件中的函数实现。
结果 :

结论 :
对于Block的自身从栈区拷贝到堆区 :
- Block块的复制首先要把需要被复制的Block转成Block的本质结构
Block_layout结构体。- 然后判断Block块的类型属性
- 如果已经是堆Block了,那么只利用自身的引用计数管理,改变Block的flags中的第2位
BLOCK_REFCOUNT_MASK就可以。- Block的引用计数管理是自己进行管理,不实用runtime底层的引用计数方式。
- 如果是全局Block,则不发生任何的拷贝,直接返回原Block。
- 如果是栈Block,则在堆区申请一块和原来栈Block内存大小一样的内存,利用位拷贝,将栈Block块整体的拷贝到堆区,保证堆Block和栈Block的数据完全一致。
- 并且重置引用计数为1,为了让内存工具可以看到完整正确的Block信息,最后才将Block的isa指向
NSMallocBlock类。
三、Block捕获的外界变量的copy
在上面,我们已经知道了Block块是如何从栈区拷贝到堆区的,在上面的图2.0.1中,可以找到Block的isa、flags、invoke都很明显的进行了从栈区到堆区的拷贝,那么除了一个保留值reserved,还有一个Block的描述并没有明显的进行拷贝而是调用了_Block_call_copy_helper。
对于被Block捕获的外界变量也需要随Block一起拷贝到堆区,才能保证外界变量的生命周期在Block内部得以延长,也就是说,_Block_call_copy_helper的这一步就代表着要对已然存储在栈Block上的外界变量copy到堆Block上。
问题 :
Block捕获的外界变量是如何随着栈区Block拷贝到堆区Block上的?
已知条件 :
_Block_call_copy_helper的调用会让Block捕获的外界变量随着栈区Block一起拷贝到堆区Block上。
操作 :
逐步进入
_Block_call_copy_helper的实现,找到和copy相关的线索。
结果 :


由已知条件可知思路 :
- 从Block的构造函数找到
descriptor2的赋值。- 从
descriptor2找到被Block捕获的外界变量是如何随着Block一起拷贝到堆上的。
进行探索 :
操作1 :
- 创建一个
iOS的Project。- 在
main.m文件中直接写入以下代码。- 通过
clang将main.m文件编译成main.cpp文件,进度条拉到最后,查看__block的c++实现。- 删除掉
main函数中无关的代码,删除强转,只保留Block相关的代码。
-
main.m代码 :
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
//这里不要直接用jd_name = @"JD"
//要用[NSString stringWithFormat:@"JD"],否则clang未必能编译成功
__block NSString *jd_name = [NSString stringWithFormat:@"JD"];
void(^jd_block)(void) = ^{
jd_name = @"eason";
NSLog(@"%@",jd_name);
};
jd_block();
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
-
clang命令,这里注意,main.m的代码中明显的有<UIKit>框架的使用,直接用以前的clang命令是无法编译到<UIKit>框架的,所以clang指令会变成下面的 :
xcrun -sdk iphonesimulator clang -rewrite-objc main.m
结果1 :

操作2 :
搜索Block构造函数的第二个参数
__main_block_desc_0_DATA。
结果2 :

操作3 :
搜索
__main_block_desc_0_DATA的属性值__main_block_copy_0和__main_block_dispose_0。
结果3 :

发现拷贝辅助函数,也就是
Block_descriptor_2存储的是对__block修饰的外界变量进行assign和dispose的函数——_Block_object_assign()和_Block_object_dispose()。
操作4 :
在
libclosure源码中搜索_Block_object_assign。找到拷贝辅助函数的实现。
结果4 :

操作4.1 :
首先,看一下拷贝辅助函数的
switch条件,也就是BLOCK_ALL_COPY_DISPOSE_FLAGS是什么。
结果4.1 :
enum {
BLOCK_ALL_COPY_DISPOSE_FLAGS =
BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_BYREF |
BLOCK_FIELD_IS_WEAK | BLOCK_BYREF_CALLER
};
enum {
BLOCK_FIELD_IS_OBJECT = 3,
BLOCK_FIELD_IS_BLOCK = 7,
BLOCK_FIELD_IS_BYREF = 8,
BLOCK_FIELD_IS_WEAK = 16,
BLOCK_BYREF_CALLER = 128
}
1.
BLOCK_FIELD_IS_OBJECT = 3: 对象
2.BLOCK_FIELD_IS_BLOCK = 7: 普通变量
3.BLOCK_FIELD_IS_BYREF = 8: __block修饰的结构体
4.BLOCK_FIELD_IS_WEAK = 16: __weak修饰的变量
5.BLOCK_BYREF_CALLER = 128: 处理Block_byref结构体内部对象的内存的时候添加的额外标记,要配合上面的枚举一起使用到
操作4.2 :
查看Block捕获对象的时候,是如何进行拷贝的。也就是图3.0.5中
_Block_retain_object的实现。
结果4.2 :

操作4.3 :
查看Block捕获普通变量的时候,是如何进行拷贝的。也就是图3.0.5中的
_Block_copy的实现。
结果4.3 :
在上面的二、栈Block变成堆Block的源码解析 中已经介绍过了。
操作4.4 :
查看Block捕获经
__block修饰的变量的时候,是如何进行拷贝的。
结果4.4 :

操作5 :
这里继续对
__block修饰的变量进行拷贝的实现的探索。进入_Block_byref_copy的实现。
结果5 :

Tips :
还记得上一节提到的,
__block修饰的变量,在Block函数内部可以进行修改的原因是指针的copy,而指针则必然指向变量的值所在的地址,更改的是这个地址上的值。那么这个地址上的数据也要跟随Block从栈拷贝到堆才对。
操作6 :
- 在汇编的
.cpp文件中找到__Block_byref_jd_name_0结构体。- 然后在
libclosure中找到Block_byref结构体。
结果6 :

可以看到,Block捕获的外界变量在汇编后被转为的结构体的本质就是Block_byref结构体。它的结构设计和Block块的结构设计是及其类似的。
操作7 :
- 根据图3.0.7和图3.0.8,在图3.0.7中,发现了这样一步调用
(*src2->byref_keep)(copy, src);,在图3.0.8中,可以知道byref_keep函数是__Block_byref_id_object_copy。- 在
.cpp中搜索__Block_byref_id_object_copy函数的实现。
结果7 :

操作8 :
- 可以看到,对
Block_byref结构体中的NSString *jd_name的copy还是利用图3.0.4中的方法,并且这次走的是第一个case,也就是BLOCK_FIELD_IS_OBJECT的copy。原因很简单,看画红框的+40,就是让Block_byref结构体的指针偏移40字节。而Block_byref结构体指针偏移40字节就是NSString *jd_name;的地址。- 所以,这一步就完成了
__block捕获的外界变量的copy操作。
结果8 :

四、总结
- 全局Block和堆栈Block的区分是编译器在编译期做出的判断
- 没有捕获外界变量的Block和定义在全局区的Block都是全局Block。
- 捕获了外界变量的Block是栈Block。
- 为了延长栈Block中的捕获到的外界变量的生命周期,防止操作系统的自动释放,所以将栈Block拷贝到堆Block,并把栈Block的指针指向堆Block,这样更稳定、更安全。
- 栈Block拷贝到堆Block是调用了
_Block_copy函数,将整个Block块拷贝到堆上。- 堆Block拥有引用计数,并且由Block的
flags属性进行记录管理,不使用runtime底层的引用计数管理。- 对于Block捕获外界变量,分为两种情况,一种是没有
__block修饰的外界变量。一种是有__block修饰的外界变量。无论是否有__block的修饰,它们的共同点都是可能会利用存储在descriptor2中的拷贝辅助函数,将存储在栈Block上的外界变量拷贝到堆Block上。
- 4.1 对于没有
__block修饰的外界变量
- 就当作是对象进行拷贝,外界变量的引用计数依然是
runtime进行管理。- 调用的是
_Block_object_assign函数。- 4.2 对于拥有
__block修饰的外界变量
- 需要两次复制,先进行外界变量的结构体的copy,也就是
Block_byref对象的拷贝。- 再利用
Block_byref结构体中的拷贝辅助函数,对外界变量结构体中真正的外界变量进行copy。- 调用的也是
_Block_object_assign函数。