这是我参与8月更文挑战的第12天,活动详情查看:8月更文挑战 上一篇我们介绍了block的区分,以及解决循环应用的方式。这篇我们主要探讨下block的原理。
1. Clang分析
定义关于block的c文件:
使用Clang进行编译
clang -rewrite-objc block.c -o block.cpp
相当于
void(*block)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA)); //初始化
block->FuncPtr(block);//调用执行
block相当于__main_block_impl_0是一个函数。继续看下它的组成
是一个结构体,包含
imp,desc,ByRef(外部的变量)。
__block_impl组成:
__main_block_desc_0组成:
__Block外部修饰的变量:__Block_byref_a_0结构:
回调函数实现:
__main_block_func_0结构:
通过Clang编译我们可以知道:我们使用
__block修饰的block是一个结构体对象,包含一个impl即我们功能的实现;一个block的描述信息,以及关于外部变量的处理。他们的关系如下:
1.1 block的调用
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);等于下面
block->FuncPtr(block);//调用执行
我们Clang的时候知道block的代码块即{}是结构体__block_impl,里面包含具体实现的函数FuncPtr的地址,FuncPtr指向__main_block_func_0。我们调用通过指针进行访问实现。block函数式保存,如果不调用的话无法实现代码块,日常开发不调用的话,就无法回调。
函数声明:即block内部实现声明成了一个函数__main_block_func_0执行具体的函数实现:通过调用block的FuncPtr指针,调用block执行
1.2 block捕获外界变量
// __block int a = 18;
int a = 18;
void(^block)(void) = ^{
// a++;
printf("a-count - %d",a);
};
block();
编译后:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//实现
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy这里的int a 与 __cself->a,就是赋值操作,内容相同,但是地址不同
printf("a-count - %d",a);
}
这里 : a(_a) 是C++语法,默认会对传过来的参数a赋值, _a会传给a,赋值操作,block在底层会把变量捕获进来,变成自己的成员变量。
__main_block_func_0中的a是值拷贝,如果此时在block内部实现中作 a++操作,是有问题的,会造成编译器的代码歧义,即此时的a是只读的
结论: block捕获外界变量时,在内部会自动生成同一个属性来保存
1.3 __block修饰
int main(){
__block int a = 18;
int a = 18;
void(^block)(void) = ^{
a++;
printf("a-count - %d",a);
};
block();
return 0;
}
//编译后
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;//*__forwarding*这里指向a的地址
int __flags;
int __size;
int a;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//实现
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref 这里的 __cself->a 与外界的a是一样的,指向同一块内存区域,说明的就是指针拷贝。
(a->__forwarding->a)++;
printf("a-count - %d",(a->__forwarding->a));
}
初始化的时候(__Block_byref_a_0 *)&a, 这里取a的地址。__main_block_func_0内部对a的处理是指针拷贝,此时创建的对象a与传入对象的a指向同一片内存空间。
结论:我们使用__block进行修饰的外部变量的时候会生成__Block_byref_a_0类型的结构体,结构体用来保存原始变量的指针和值。 将变量生成的结构体对象的指针地址 传递给block,然后在block内部就可以对外界变量进行操作了。
2. Block的源码分析
关于编译的__main_block_copy_0, __main_block_dispose_0, __main_block_desc_0这些结构体,他们是干什么用的呢,我们来分析下block的copy过程。
我们通过断点,分析下汇编流程,调试代码如下
断点调试开始为栈block
调用
objc_retainBlock
我们给
objc_retainBlock添加符号断点
加
_Block_copy符号断点,运行断住,在libsystem_blocks.dylib源码中
可以到苹果开源网站下载最新的libclosure-79源码,通过查看
_Block_copy的源码实现,
2.1 Block_layout分析
发现block在底层的真正类型是
Block_layout
- 包含有isa指针,block的类型
- flags标识 : 这个flags标识符的定义有
具体解释
-
第1 位 -
BLOCK_DEALLOCATING,释放标记,-般常用 BLOCK_NEEDS_FREE 做 位与 操作,一同传入 Flags , 告知该 block 可释放。 -
低16位 -
BLOCK_REFCOUNT_MASK,存储引用计数的值;是一个可选用参数 -
第24位 -
BLOCK_NEEDS_FREE,低16是否有效的标志,程序根据它来决定是否增加或是减少引用计数位的 值; -
第25位 -
BLOCK_HAS_COPY_DISPOSE,是否拥有拷贝辅助函数(a copy helper function); -
第26位 -
BLOCK_IS_GC,是否拥有 block 析构函数; -
第27位,标志是否有垃圾回收;//OS X
-
第28位 -
BLOCK_IS_GLOBAL,标志是否是全局block; -
第30位 -
BLOCK_HAS_SIGNATURE,与 BLOCK_USE_STRET 相对,判断当前 block 是否拥有一个签名。用于 runtime 时动态调用。
- invoke调用函数
- descriptor其它相关描述,是否正在析构等
descriptor:block的附加信息,比如保留变量数、block的大小、进行copy或dispose的辅助函数指针。有三类
Block_descriptor_1是必选的Block_descriptor_2和Block_descriptor_3都是可选的
2.2 block的内存变化
我们断点一个全局block
读取寄存器,此时的
block 是全局block ,即__NSGlobalBlock__类型
增加外部变量
此时读取block断点处 还是
__NSStackBlock__
继续跟流程
- 增加
_Block_copy符号断点并断住,直接在最后的ret加断点,读取rax,发现经过_Block_copy之后,变成了堆block,即__NSMallocBlock__,主要是因为block地址发生了改变,为堆block
结论:block代码块存在外部变量的时候,
编译的时候是栈block,运行时会通过_Block_copy把栈区block拷贝到堆区block。
2.3 签名
上面我们知道Block_layout中 descriptor 相关附加信息,存在 Block_descriptor_1是必选的 Block_descriptor_2 和 Block_descriptor_3都是可选的。
在一段内存中存储是连续的,我们在源码中别的地方获取Block_descriptor_2也是通过平移的方式
flag是BLOCK_HAS_COPY_DISPOSEdesc2会存储copy信息,当flag是BLOCK_HAS_SIGNATURE存在desc3会存储签名。 我们用lldb验证:
我们读取堆block的内存情况,之前我们知道block的结构组成因此,
0x000000010bff3028对应的是descriptor
继续读取desc的内存分布,其中desc1是由resloved和size组成,我们读取下0x000000010bff2de0,并进行转换
char*打印出之前的签名。这里说明desc2不存在,验证下。
- 判断是否有
Block_descriptor_2,即flags的BLOCK_HAS_COPY_DISPOSE(拷贝辅助函数)是否有值
Block_layout->flags为BLOCK_HAS_COPY_DISPOSE才有desc2,因此我们 p/x 1<<25 ,即1左移25位,其十六进制为 0x2000000
值为0表明没有desc2.
- 验证Block_descriptor_3,flags是
BLOCK_HAS_SIGNATURE存在desc3会存储签名
有值说明存在desc3.
- 验证签名:
[NSMethodSignature signatureWithObjCTypes:"v8@?0"],即打印签名
block的签名信息类似于方法的签名信息,主要是体现block的返回值,参数以及类型等信息
2.3 _Block_copy分析
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;
if (!arg) return NULL;
// The following would be better done as a switch statement
aBlock = (struct Block_layout *)arg;
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high//释放
latching_incr_int(&aBlock->flags);
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;//全局block直接返回
}
else {// 栈 - 堆 (编译期)
// Its a stack block. Make a copy.
size_t size = Block_size(aBlock);
struct Block_layout *result = (struct Block_layout *)malloc(size);//堆区开辟空间
if (!result) return NULL;
memmove(result, aBlock, size); // bitcopy first 栈区拷贝到堆区
#if __has_feature(ptrauth_calls)
// Resign the invoke pointer as it uses address authentication.
result->invoke = aBlock->invoke;
#if __has_feature(ptrauth_signed_block_descriptors)
if (aBlock->flags & BLOCK_SMALL_DESCRIPTOR) {
uintptr_t oldDesc = ptrauth_blend_discriminator(
&aBlock->descriptor,
_Block_descriptor_ptrauth_discriminator);
uintptr_t newDesc = ptrauth_blend_discriminator(
&result->descriptor,
_Block_descriptor_ptrauth_discriminator);
result->descriptor =
ptrauth_auth_and_resign(aBlock->descriptor,
ptrauth_key_asda, oldDesc,
ptrauth_key_asda, newDesc);
}
#endif
#endif
// reset refcount
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
_Block_call_copy_helper(result, aBlock);
// Set isa last so memory analysis tools see a fully-initialized object.
result->isa = _NSConcreteMallocBlock;//设置isa为堆block
return result;
}
}
- block_copy主要作用是把
栈区block拷贝到堆区block。
1.判断block是否在释放,在的话直接释放。
2.block是否是全局block,是的话直接返回。
3.计算栈区block的大小,并在堆区开辟相同大小的内存空间,通过memmove把栈区的block的内容拷贝到堆区。
- 设置相关flags,并设置block的类型
_NSConcreteMallocBlock
2.4 _Block_object_assign 分析
block捕捉外部变量的时候,会调用_Block_object_assign进行拷贝操作,根据变量的修饰类型进行相对应的处理,修饰类型如下:
继续看
_Block_object_assign源码
在进行捕获外界变量操作的时候主要更具变量的类型以及修饰做对应操作:
BLOCK_FIELD_IS_OBJECT:普通变量的话,引用计数+1操作。BLOCK_FIELD_IS_BLOCK:捕获变量是void (^object)(void) = block的话,进行_block_copy操作。 3.BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:__weak __block ... x;以上修饰的变量x则进行_Block_byref_copy操作
_Block_byref_copy实现源码
- 将捕获的对象进行
Block_byref转换并保存一份。 - 如果外界变量没有拷贝到堆内存,则申请内存,进行
拷贝,拷贝了,则处理后返回。 - 其中
copy和src的forwarding指针都指向同一片内存,这也是为什么__block修饰的对象具有修改能力的原因
- 使用__block修饰变量
用Clang编译下,修饰的变量会生成Block_byref的结构
综上所述:外界变量
__block修饰后持有流程:_Block_copy(把block从栈区拷贝到堆区)->_Block_byref_copy(block对象拷贝成Block_byref结构体)->_Block_objct_asign(对__block修饰的变量进行拷贝)
2.5 _Block_object_dispose 分析
拷贝的时候retain操作,对应的就是dispose,通过block变量的修饰选择对应的方法
和copy的时候类似
- 查看
_Block_byref_release
3. 总结
1.block本质是一个函数,一个结构体,也是一个对象,block没有名字也可以成为匿名函数。
2.block底层是结构体Block_layout.包含isa(block的类型,栈区block还是堆区),flags(block的一些标识),reserved(保留字段),invoke(回调执行),desc(描述信息)。
3. block代码块存在外部变量的时候,编译的时候是栈block,运行时会通过_Block_copy把栈区block拷贝到堆区block
4. block的签名信息类似于方法的签名信息,主要是体现block的返回值,参数以及类型等信息
5. 我们使用__block修饰的外界变量的时候会通过_Block_object_assign进行拷贝到_Block_byref的结构体中。
6. 我们销毁的时候进行release,释放我们持有的外部变量。