Block clang分析
通过命令clang -rewrite-objc main.m -o main.cpp
将下面的代码编译成cpp
block底层结构
编译成cpp文件之后,找到对应的main方法,然后找到对应block代码如下:
int a = 18;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
这个代码看的很懵逼,我们吧对应的类型去掉简化一下:
__main_block_impl_0
方法实现block->FuncPtr(block)
调用block
底层是一个结构体__main_block_impl_0
,然后继承__block_impl
,__block_impl
定义如下
struct __block_impl {
void *isa; //指向区域
int Flags; //标记
int Reserved;
void *FuncPtr; //调用方法
};
__main_block_impl_0(实现)
- 通过
__main_block_impl_0
初始化赋值block __main_block_func_0
传入了block本身,然后进行block代码的实现。
__block修饰外部变量
如果想要在block内部修改外部变量,我们需要用__block
修饰,那么__block
究竟做了啥。用__block
修饰外部变量,然后重新clang
一下。
发现变量用
__Block_byref_a_0
结构体包装了一下,然后传入给
block。__Block_byref_a_0
:内部定义
实际上forwarding传入的就是外部变量的地址。
然后我们在调用函数里面访问的也是a的地址,所以我们在block改变的变量,实际上就是外部变量。
block copy
在block声明的地方下断点,然后通过汇编和符号断点objc_retainBlock->_Block_copy
。_Block_copy在libsystem_blocks.dylib
中,由于这个库没有开源,可以用libclosure代替。
- block 的本质是一个Block_layout
- 如果是全局block,直接返回
- 如果是栈block,捕获了外部变量
在
_Block_copy
打断点,通过register read x0
,在最开始的时候传入时候block是什么类型然后在return的时候打印返回的类型:
然后分别打印x0寄存器
总结:在编译的时候是stack block,如果捕获了外部变量就会从拷贝到堆区,变成堆block.
block_layout结构
struct Block_layout {
void *isa; //指向确定block类型
volatile int32_t flags; // 标识码,包含是否有签名、正在析构
int32_t reserved; //保留字段
BlockInvokeFunction invoke;// 函数,也就是FuncPtr
struct Block_descriptor_1 *descriptor; //附加信息
// imported variables
};
#define BLOCK_DESCRIPTOR_1 1
**struct** Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};
\
#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
BlockCopyFunction copy;
BlockDisposeFunction dispose;
};
\
#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
其中Block_descriptor_2
和Block_descriptor_3
都是可选字段。全局搜索get方法:
这里
Block_descriptor_2
,Block_descriptor_3
是在Block_descriptor_1
的基础上做了内存平移处理。
block 捕获变量生命周期
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc ViewController.m -o ViewController.cpp
通过上面的命令堆block进行clang分析,这里捕获objdect类型,没有用__block修饰。
- 通过描述字段
__ViewController__viewDidLoad_block_desc_0
,这个实际上就是对应源码里面Block_descriptor_1
、Block_descriptor_2
、Block_descriptor_3
。 - 看copy方法
__ViewController__viewDidLoad_block_copy_0
,发现最终调用了_Block_object_assign
_Block_object_assign分析
源码对不同的类型做了不同的处理
- BLOCK_FIELD_IS_OBJECT ,如果是对象类型,直接赋值
*dest = object;
- BLOCK_FIELD_IS_BLOCK,如果捕获的是block类型,则调用
_Block_copy
拷贝 - BLOCK_FIELD_IS_BYREF,如果使用__block修饰,则会调用
_Block_byref_copy
, - 其他情况都是直接赋值
_Block_byref_copy分析
object对象传入会包装一成一个Block_byref
对象,原对像。
struct Block_byref *src = (struct Block_byref *)arg;
重新开辟空间,生成一个dst对象
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
设置forwarding
,保证block
内部和外部都指向同一个对象
copy->forwarding = copy; //
src->forwarding = copy; //
byref_keep 和 byref_keep处理
copy2->byref_keep = src2->byref_keep;
copy2->byref_destroy = src2->byref_destroy;
// 捕获到了外界的变量 - 内存处理 - 生命周期的保存
(*src2->byref_keep)(copy, src);
但是这个保存做了啥,源码里面搜索不到,我们用__block修饰,然后再生成.cpp文件,打开
- 包装的
Block_byref
会有_Block_byref_id_object_copy
和__Block_byref_id_object_dispose
发现会再次调用
_Block_object_assign
拷贝。
调用:_Block_object_dispose
释放
总结:所以在使用__block的修饰的变量的时候,会有二次拷贝。加上block从栈拷贝到堆就是3次.