本文为L_Ares个人写作,以任何形式转载请表明原文出处。
上一节重新的巩固了一下Block
的基础知识和简单的使用方式,以及解决循环引用的方法,本节则将通过clang
编译的文件和libclosure
源码去探索Block
的一些更本质的内容。
准备
1. libclosure源码
libclosure-73源码文件
2. block的.cpp
文件
1. 创建一个C语言
的Commond Line Tool
项目。
2. 语言选择C
。
3. 创建结果。
4. 在main.c
中创建一个最简单的Block
对象,并且执行Block
。
5. commond + B
进行编译,然后打开terminal终端
,进行clang
编译成.cpp
文件。
- 先要进入到
main.c
所在的文件夹下
- 然后打开
terminal终端
,输入以下clang
指令。
clang -rewrite-objc main.c -o main.cpp
6. clang结果
一、Block的clang探索
通过clang
出来的.cpp
,主要探索4点。
- Block的本质是什么。
- Block()的意义。
- Block捕获外部变量的原理。
__block
的原理。
1. Block的本质
操作1 :
打开上面
clang
得到的main.cpp
文件。滑至文件最后。
结果1 :
操作2 :
去掉
(void(*)())
和(void *)
的强制类型转换,Block
的结构就变成了
结果2 :
操作3 :
commond + F
搜索__main_block_impl_0
。
结果3 :
至此,可以看到Block块的一个构造方式,其中的__block_impl
结构体存储了Block块的所有信息。
操作4 :
commond + F
搜索__block_impl
。
结果4 :
操作5 :
根据
图1.1.2
和图1.1.3
中,__block_impl
结构体的存储属性,以及官方给的注释,进入libclosure
的Block_private.h
。查找拥有这样结构的结构体。
结果5 :
结论 :
block
的本质是Block_layout
结构体。
2. Block()的意义
操作1 :
打开刚才的
.cpp
文件,找到block
的构造函数那一行。
结果1 :
操作2 :
去掉
block()
经过clang
后,得到的那行代码的所有强转符号。
结果2 :
结论 :
1. 写在
block
内部的函数被保存到block
的FuncPtr
中,这仅仅只是函数的实现被保存进入block
。
2.block()
才是真正的对函数进行调用。
3. Block捕获外部变量的原理
在main.c
中添加外部变量,更改main.c
的代码为 :
#include
int main(int argc, const char * argv[]) {
int a = 10;
void(^block)(void) = ^{
printf("a = %d",a);
};
block();
return 0;
}
操作1 :
- 重新
commond + B
编译main.c
文件,然后进行clang
转换成.cpp
文件。- 打开新的
.cpp
文件,滑至文件最后。- 去掉强转。
结果1 :
操作2 :
再次查看
block
的构造函数__main_block_impl_0
结果2 :
操作3 :
查看对外部变量
a
的调用。
结果3 :
结论 :
1. 当
block
捕获外部变量的时候,block
结构体内部会生成一个相同类型、相同名称的属性。2. 利用
block
自身的构造函数,将外部变量传入block
结构体内部,用外部变量的值赋值给block
内部自动生成的同类型元素。3.
block
内部调用外部变量时,在fp
函数体的内部也会生成又一个对象,对外部变量进行值拷贝
,这个对象和外部变量不是同一个对象。4. 所以,当我们对
block
内部捕获的外部变量进行操作的时候,是对另外一个对象进行操作,而不是对外部变量本身进行操作。
4. __block的原理
在main.c
中继续修改代码如下 :
#include
int main(int argc, const char * argv[]) {
__block int a = 10;
void(^block)(void) = ^{
printf("a = %d",a);
};
block();
return 0;
}
操作1 :
- 重新
commond + B
编译main.c
文件,然后进行clang
转换成.cpp
文件。- 打开新的
.cpp
文件,滑至文件最后。- 去掉强转。
结果1 :
操作2 :
搜索
__Block_byref_a_0
结构体。
结果2 :
操作3 :
查看图1.4.0中的声明
block
和block
的构造函数,以及block()
的实现
结果3 :
结论 :
1.
__block
会将外部变量变成一个结构体对象A
。2.
结构体对象A
内部的__forwarding
存储着指向外部变量的地址的指针。3.
block
结构体则在内部生成新的、该类型的结构体指针对象B
。4. 通过构造函数,利用
结构体对象A
的指针,将结构体对象A
内部的__forwarding
赋值给结构体指针对象B
,也就是把指向外部变量的地址的指针赋值给结构体指针对象B
。5. 当调用外部变量的时候,
block
的函数内部会生成一个该类型的结构体指针对象C
,结构体指针对象C
通过结构体指针对象B
的赋值,拥有了指向外部变量的地址的指针。6. 所以在
block
内部去改变被__block
修饰的外部变量,实际上操作的是外部变量的地址上内容。7.
__block
是指针拷贝的实现。
二、Block的内存变化
在上一节Block(一)中,已经介绍了Block一共有6种类别,而开发者常用的只有其中的3种,分别是 :
1.
NSGlobalBlock
: 全局block
2.NSStackBlock
: 栈block
3.NSMallocBlock
: 堆block
1. NSGlobalBlock
先利用查看汇编,来查看NSGlobalBlock
的声明和创建流程。
步骤 :
1. 随意创建一个项目,在ViewController
的viewDidLoad
中,简单的创建一个最基本的block
,在声明block
的地方挂上断点。
- (void)viewDidLoad {
[super viewDidLoad];
void(^block)(void) = ^{ //挂上断点
NSLog(@"this is a block");
};
block();
}
2. 打开xcode
--> Debug
--> Debug Workflow
--> Always Show Disassembly
,来看汇编。
3. 使用真机
进行调试。运行项目。
4. 对objc_retainBlock
加符号断点,执行到该断点位置,查看objc_retainBlock
有怎样的调用。
走到这里就不用再往后走了,看汇编的最后一句 :
5. 此时读取x0
寄存器(这也是为什么要用真机的原因,模拟器读不到x0
寄存器)。
结论 :
1.
NSGlobalBlock
类型的block对象,经过objc_retainBlock
,调用了_Block_copy
方法,最后将block
对象完成创建并返回。2. 当
block
没有获取外部变量的时候,block
是一个NSGlobalBlock
类型的block对象。当block
被在声明全局变量的地方进行调用的时候,block
也是NSGlobalBlock
类型。3.
NSGlobalBlock
存储位置在内存中的静态区(.data区)
2. NSStackBlock和NSMallocBlock
因为编译器长期处在ARC
环境下,所以这两个一起说,因为不好捕捉NSStackBlock
的瞬间,ARC
会自动的将NSStackBlock
复制到堆中,变成NSMallocBlock
。
步骤 :
1. 对上面NSGlobalBlock
的测试代码做调整,增加一个外部变量,并在block内部捕获外部变量。
- (void)viewDidLoad {
[super viewDidLoad];
int a = 10;
void(^block)(void) = ^{ //挂上断点
NSLog(@"%d",a);
};
block();
}
2. 删除之前的所有符号断点,在上述代码注释处挂上断点,执行项目
3. 在objc_retainBlock
内部查看此时的寄存器x0
位置,传入的是什么类型的block
。
4. 做一个objc_retainBlock
符号断点,进入objc_retainBlock
。一直step into
到__Block_copy
,并在__Block_copy
的最后一句汇编,也就是那句返回,挂上断点。然后再读寄存器x0
结论 :
1. 当
block
捕获了外部变量之后,block
的类型就从NSGlobalBlock
变成了NSStackBlock
。2.
NSStackBlock
存在于__Block_copy
完成之前。3.
__Block_copy
内部会将NSStackBlock
变为NSMallocBlock
再返回。4.
NSStackBlock
和NSMallocBlock
的地址是不一样的,发生了一步copy的操作。也就是说,NSStackBlock
通过copy可以得到NSMallocBlock
。
3. 为什么要对NSStackBlock进行copy
直接说结论
为了延长
block
的生命周期。1. 如果
block
的作用域结束,那么该block
就会被废弃,其内部被__block
修饰的外部变量也会随之被废弃。2. 将
block
从栈区复制到堆区,即使存放在栈区的block
已经被废弃,堆区的block
依然可以使用,被__block
修饰的外部变量也不会被废弃。
三、Block的源码探索
1. Block结构体的解析
在上面我们已经知道了Block
的本质是Block_layout
结构体,这里将对这个结构体的属性做一个介绍。
看图3.1.0,block的结构体指针中,可见5个结构体的属性。
1.1 isa
这个isa
,在.cpp
的文件中,赋值的是block
的类型,前文说过,block
一共有6种,分别是 :
1.
_NSConcreteStackBlock
2._NSConcreteMallocBlock
3._NSConcreteAutoBlock
4._NSConcreteFinalizingBlock
5._NSConcreteGlobalBlock
6._NSConcreteWeakBlockVariable
而我们常用的block
类型只属于其中的3种 :
1.
_NSConcreteGlobalBlock
: 全局Block,对应NSGlobalBlock
。
2._NSConcreteStackBlock
: 栈Block,对应NSStackBlock
。
3._NSConcreteMallocBlock
: 堆Block,对应NSMallocBlock
。
1.2 flags
这是block的标识位。
- 类型是
int32_t
,表明它有32bit。- 用
volatile
修饰,为了保证多线程操作的时候,flags
永远都会从内存中读取真正的block
标识数据,而不是从CPU寄存器中读取,保证数据的正确性。
flags
会充分利用内存,利用bit位
存储block
的一些信息,类似isa
中的ISA_BITFIELD
利用位域存储isa
信息。
flags
的bit位
存储的内容 :
// Values for Block_layout->flags to describe block objects
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
BLOCK_NEEDS_FREE = (1 << 24), // runtime
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code
BLOCK_IS_GC = (1 << 27), // runtime
BLOCK_IS_GLOBAL = (1 << 28), // compiler
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler
};
第1位 : 存放着释放标记。一般利用
BLOCK_NEEDS_FREE
(1左移24位
),做位与
操作,再存入该位,记录block
是否需要释放。第16位 : 存储
block
的引用计数的值。第24位 : 是否需要释放
block
。它会影响第1位的值,也会影响第16位的值。第25位 : 是否拥有拷贝辅助函数。
第26位 : 是否拥有
block
析构函数。第27位 : 是否有垃圾回收。
第28位 : 是否是全局变量。
第30位 :
block
是否拥有一个签名。如果没有签名,则第29位也不会被定义使用。
1.3 reserved
我也不知道干什么用的,如果有大佬知道的话,可以赐教,万分感谢。
1.4 invoke
看其类型——BlockInvokeFunction
typedef void(*BlockInvokeFunction)(void *, ...);
重定义类型的函数指针,也就是存储在block
块内部的函数。
1.5 descriptor
block
的描述信息,是结构体。拥有3种descriptor
,其中,Block_descriptor_1
是block
一定拥有的描述信息。
1.5.1 Block_descriptor_1
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};
存放了一个保留值和一个block
的大小。
1.5.2 Block_descriptor_2
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
BlockCopyFunction copy;
BlockDisposeFunction dispose;
};
存放了一个copy
方法和一个dispose
方法。
官方注释 : 当flags
中的BLOCK_HAS_COPY_DISPOSE
,也就是flags
第25位为1的时候,block
就会拥有Block_descriptor_2
描述。
1.5.3 Block_descriptor_3
struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
存放了block
的签名和layout
,layout
依赖于flags
的第31位是否为1。
官方注释 : 当falgs
中的BLOCK_HAS_SIGNATURE
,也就是flags
的第30位为1的时候,block
就会拥有Block_descriptor_3
。
2. Block的签名
OC对象都是有签名的,既然说Block
是OC对象,那么Block
也必然有签名,Block
签名存储在Block
结构体的Block_descriptor_3
属性中。
下面来验证、并且获得Block
的签名。
操作1 :
- 随便创建一个项目,并在
ViewController
的viewDidLoad
中创建一个block
。- 在下面代码注释处添加断点。
- 打开汇编。
xcode
-->Debug
-->Debug Workflow
-->Always Show Disassembly
- 连接真机,执行代码。
lldb
打印x0
寄存器中的内容。
- (void)viewDidLoad {
[super viewDidLoad];
void(^block)(void) = ^{ //挂上断点
NSLog(@"this is a block");
};
block();
}
结果1 :
这里已经可以获得Block
的type encoding
了,就是图3.2.0中的signature : "v8@?0"
。关于type encoding
可以看我的这片博客关于Objective-C type encoding。
其中v8
表示void
总共占用8位,@?
表示一个不明类型的对象,0
表示@?
是在这个函数的第0位地址上。
那么,利用 :
[NSMethodSignature signatureWithObjCTypes:"@?"];
就可以获得Block
的签名信息。
操作2 :
在
lldb
上po [NSMethodSignature signatureWithObjCTypes:"@?"]
结果2 :
结论 :
1. Block拥有签名,并且存储在
Block_descriptor_3
中。
2. Block的type encoding
是@?
。
3. Block的签名是isObject
,isBlock
。