本文为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
文件中的函数实现。
结果 :
![](https://img.haomeiwen.com/i16702189/20ce72230de7a961.png)
结论 :
对于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
相关的线索。
结果 :
![](https://img.haomeiwen.com/i16702189/f11e41e82315c2aa.png)
![](https://img.haomeiwen.com/i16702189/a29aa47525699cdf.png)
由已知条件可知思路 :
- 从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 :
![](https://img.haomeiwen.com/i16702189/c711d65b256180d2.png)
操作2 :
搜索Block构造函数的第二个参数
__main_block_desc_0_DATA
。
结果2 :
![](https://img.haomeiwen.com/i16702189/2ff78590854815e8.png)
操作3 :
搜索
__main_block_desc_0_DATA
的属性值__main_block_copy_0
和__main_block_dispose_0
。
结果3 :
![](https://img.haomeiwen.com/i16702189/196fe4a0a15e94c5.png)
发现拷贝辅助函数,也就是
Block_descriptor_2
存储的是对__block
修饰的外界变量进行assign
和dispose
的函数——_Block_object_assign()
和_Block_object_dispose()
。
操作4 :
在
libclosure
源码中搜索_Block_object_assign
。找到拷贝辅助函数的实现。
结果4 :
![](https://img.haomeiwen.com/i16702189/39b774d402a372e3.png)
操作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 :
![](https://img.haomeiwen.com/i16702189/9b4dceb619236c4d.png)
操作4.3 :
查看Block捕获普通变量的时候,是如何进行拷贝的。也就是图3.0.5中的
_Block_copy
的实现。
结果4.3 :
在上面的二、栈Block变成堆Block的源码解析 中已经介绍过了。
操作4.4 :
查看Block捕获经
__block
修饰的变量的时候,是如何进行拷贝的。
结果4.4 :
![](https://img.haomeiwen.com/i16702189/9879366912bbd96a.png)
操作5 :
这里继续对
__block
修饰的变量进行拷贝的实现的探索。进入_Block_byref_copy
的实现。
结果5 :
![](https://img.haomeiwen.com/i16702189/7412843a191a7d17.png)
Tips :
还记得上一节提到的,
__block
修饰的变量,在Block函数内部可以进行修改的原因是指针的copy,而指针则必然指向变量的值所在的地址,更改的是这个地址上的值。那么这个地址上的数据也要跟随Block从栈拷贝到堆才对。
操作6 :
- 在汇编的
.cpp
文件中找到__Block_byref_jd_name_0
结构体。- 然后在
libclosure
中找到Block_byref
结构体。
结果6 :
![](https://img.haomeiwen.com/i16702189/5d4229401537383d.png)
可以看到,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 :
![](https://img.haomeiwen.com/i16702189/45ca57c10ab47db5.png)
操作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 :
![](https://img.haomeiwen.com/i16702189/edc1d148e80f9cfe.png)
四、总结
- 全局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
函数。
网友评论