本篇文章我们来探索下block
的底层原理实现,栈区block是如何拷贝的堆区的,block捕获外部变量的本质,block的数据结构等内容。
block底层实现
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^block)(void) = ^{
NSLog(@"block探索");
};
block();
}
return 0;
}
接下来我们clang
一下
-
block
是一个结构体,这个结构体定义为__main_block_impl_0
,该结构体继承自__block_impl
struct __block_impl {
void *isa; //isa指向
int Flags;
int Reserved;
void *FuncPtr;//函数保存
};
- 在结构体
__main_block_impl_0
中有个构造函数,该构造函数对block结构体中相关属性进行设置
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
//构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
- 我们看到构造函数的第一个参数
*fp
,在构造的时候传入的是__main_block_func_0
,其实是函数的实现地址
,在定义block
时,,将block的任务函数封装到FuncPtr
属性中
- 在执行block时,其实是调用
block->FuncPtr
,然后将block
自身作为参数传入
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
【总结】通过上述分析,block其实是个结构体,也可以认为是一个函数。block将任务保存到结构体的FuncPtr
属性中,block->FuncPtr(block)
,block作为隐藏参数,函数执行过程中,会持有block中的全部数据。
捕获变量
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
void(^block)(void) = ^{
NSLog(@"block探索:%@",obj);
};
block();
}
return 0;
}
重新clang
下
- 首先在捕获到外部变量时,
__main_block_impl_0
构造时传入obj
- 然后在构造函数中通过
值拷贝
的方式,对成员变量_obj
赋值 - 在执行任务时,从结构体中取出相应的成员变量进行处理
NSObject *obj = __cself->obj;
接下来验证下是否是值copy
通过打印
内外部obj
发现,两个obj都指向同一片内存
,但是指针地址是不同的
,这个时候就可以断定,这里是值拷贝
这里不允许变更obj
是因为,block
内部有个obj
,外部也有,这就会造成歧义,不知道该改变哪个obj
通过__block捕获变量
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block NSObject *obj = [NSObject alloc];
void(^block)(void) = ^{
NSLog(@"block探索:%@",obj);
};
block();
}
return 0;
}
重新clang
下
- 这里用
__block修饰时
,会初始化__Block_byref_a_0
的结构体 - 在
block
结构体中多出一个__Block_byref_a_0
来声明的属性obj
的取地址 -
obj
的地址会赋值到__Block_byref_a_0
结构体中的__forwarding
属性 - 在调用函数时,取得也是
forwarding
中的obj
,obj->__forwarding->obj
来看下__Block_byref_obj_0
结构体
struct __Block_byref_obj_0 {
void *__isa;
__Block_byref_obj_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *obj;
};
这里我们再来验证下是值拷贝
还是指针拷贝
这里发现
内部和外部obj
的指针地址是一样的
,由此可以看出当使用__block
修饰时,是进行的指针拷贝
block底层真正类型
我们在代码blcok
中打一个断点,并打开Always show Disassembly
接下来给
objc_retainBlock
下一个符号断点,来到了_Block_copy
给
_Block_copy
符号断点,发现是在libsystem_blocks.dylib
源码执行
但是libsystem_blocks.dylib
并不开源,我们可以查看libclosure
源码
Block_layout
struct Block_layout {
void * __ptrauth_objc_isa_pointer isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
BlockInvokeFunction invoke;
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_layout
包括
-
isa
指向的block
类型 -
flags
标识码 -
reserved
保留字段 -
invoke
函数,即:FuncPtr
-
descriptor
附加信息
先来看下flag
定义
enum {
//释放标记,一般常用于BLOCK_BYREF_NEEDS_FREE做位与运算,一同传入flags,告知该block可释放
BLOCK_DEALLOCATING = (0x0001), // runtime
//存储引用引用计数的 值,是一个可选用参数
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
//低16位是否有效的标志,程序根据它来决定是否增加或者减少引用计数位的值
BLOCK_NEEDS_FREE = (1 << 24), // runtime
//是否拥有拷贝辅助函数,(a copy helper function)决定block_description_2
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler
//是否拥有block C++析构函数
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code
//标志是否有垃圾回收,OSX
BLOCK_IS_GC = (1 << 27), // runtime
//标志是否是全局block
BLOCK_IS_GLOBAL = (1 << 28), // compiler
//与BLOCK_HAS_SIGNATURE相对,判断是否当前block拥有一个签名,用于runtime时动态调用
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
//是否有签名
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler
//使用有拓展,决定block_description_3
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler
};
-
Block_descriptor_1
在block
是必然存在的reserved
是保留字段,size
是block
大小 -
Block_descriptor_2
是可选参数,通过Block_layout
中的flag
来判断,将Block_descriptor_1
内存平移的方式来获取对应的值 -
Block_descriptor_3
也是可选参数,先平移Block_descriptor_1
大小,再判断Block_descriptor_2
是否存在,如果存在再平移Block_descriptor_2
,不存在则不需要平移Block_descriptor_2
block内存变化
引入下面的案例,在block
内部访问外部变量
在经过
objc_retainBlock
方法事,我们打印寄存器发现为栈block
在经过
_Block_copy
方法后,同样打印x0
寄存器发现变成了堆block
【结论】由此得出结论,捕获外部变量在
编译时为栈block
,运行时通过_Block_copy
方法会copy
到堆区,变成堆block
。
_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;
}
//是否是全局block
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
//这里只能是栈block,因为编译期是不可能申请堆的,这是是将栈copy到堆
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;
//进行block copy
memmove(result, aBlock, size); // bitcopy first
#if __has_feature(ptrauth_calls)
// Resign the invoke pointer as it uses address authentication.
// invoke 赋值
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.
//修改isa指向堆
result->isa = _NSConcreteMallocBlock;
return result;
}
}
- 这里现将
block
强转成Block_layout
类型 - 判断是否正在释放,如果是则不处理
- 判断是否是全局
block
,如果是则不处理 - 接下来else判断,在编译期不可能是
堆block
,所以这里一定是栈block
- 获取
block
大小 - 开辟内存空间,通过
memmove
进行数据拷贝,并将invoke 和 flag
赋值 - 最后将
isa
指向堆
Block_byref结构体拷贝
回到main.cpp
文件中,查看__main_block_desc_0
结构体定义
其中
size和reserved
是Block_descriptor_1
的两个属性。void (*copy)和void (*dispose)
对应的是Block_descriptor_2
两方法。在copy
方法中会调用_Block_object_assign
来查看下该方法
先看下注释
-
BLOCK_FIELD_IS_OBJECT
:捕获OC对象 -
BLOCK_FIELD_IS_BLOCK
:捕获另一个block -
BLOCK_FIELD_IS_BYREF
: 捕获用__block声明的变量
void _Block_object_assign(void *destArg, const void *object, const int flags) {
const void **dest = (const void **)destArg;
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
case BLOCK_FIELD_IS_OBJECT:
_Block_retain_object(object);
*dest = object;
break;
case BLOCK_FIELD_IS_BLOCK:
*dest = _Block_copy(object);
break;
case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
case BLOCK_FIELD_IS_BYREF:
*dest = _Block_byref_copy(object);
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
*dest = object;
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_WEAK:
*dest = object;
break;
default:
break;
}
}
- 如果是
BLOCK_FIELD_IS_OBJECT
,指针指向该对象,将对该对象进行持有,引用计数加1 -
BLOCK_FIELD_IS_BLOCK
,捕获一个block
,则进行_Block_copy
操作 -
BLOCK_FIELD_IS_BYREF
:用__block
声明,则调用_Block_byref_copy
_Block_byref_copy
源码
static struct Block_byref *_Block_byref_copy(const void *arg) {
struct Block_byref *src = (struct Block_byref *)arg;
// __block 内存是一样 同一个家伙
if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
// src points to stack
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
copy->isa = NULL;
// byref value 4 is logical refcount of 2: one for caller, one for stack
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
copy->forwarding = copy; // patch heap copy to point to itself
src->forwarding = copy; // patch stack to point to heap copy
copy->size = src->size;
if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
// Trust copy helper to copy everything of interest
// If more than one field shows up in a byref block this is wrong XXX
struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
copy2->byref_keep = src2->byref_keep;
copy2->byref_destroy = src2->byref_destroy;
if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
copy3->layout = src3->layout;
}
// 捕获到了外界的变量 - 内存处理 - 生命周期的保存
(*src2->byref_keep)(copy, src);
}
else {
// Bitwise copy.
// This copy includes Block_byref_3, if any.
memmove(copy+1, src+1, src->size - sizeof(*src));
}
}
// already copied to heap
else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
latching_incr_int(&src->forwarding->flags);
}
return src->forwarding;
}
- 这里是将外部对象封装成
Block_byref *src
- 然后去
malloc
一个Block_byref *copy
- 设置
forwarding
保证内外部Block_byref
对象指向同一个对象,这也是为什么__block修饰的对象具有修改能力的原因
copy->forwarding = copy;
src->forwarding = copy;
- 拷贝等操作完成以后,调用了
byref_keep
函数,这个函数又干了些啥事情
Block_byref 中的 object 拷贝分析
先源码看下 Block_byref
相关结构:
// 结构体
struct Block_byref {
void * __ptrauth_objc_isa_pointer isa; // 8
struct Block_byref *forwarding; // 8
volatile int32_t flags; // contains ref count//4
uint32_t size; // 4
};
struct Block_byref_2 {
// requires BLOCK_BYREF_HAS_COPY_DISPOSE
BlockByrefKeepFunction byref_keep; // 8
BlockByrefDestroyFunction byref_destroy; // 8
};
struct Block_byref_3 {
// requires BLOCK_BYREF_LAYOUT_EXTENDED
const char *layout;
};
__block
底层是__Block_byref_obj_0
结构体,其声明的对象,我们可以看到void (*__Block_byref_id_object_copy)(void*, void*)
这个属性其实就是byref_keep
变量,传入的是__Block_byref_id_object_copy_131
,
这里byref_keep
调用__Block_byref_id_object_copy_131
struct __Block_byref_obj_0 {
void *__isa;
__Block_byref_obj_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *obj;
};
看下__Block_byref_id_object_copy_131
,我们发现最终还是进入_Block_object_assign
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
- 结合
__Block_byref_obj_0
结构和Block_byref
结构,向下平移40个字节,得到的是NSObject *obj,对obj也是调用了_Block_object_assign函数,传入的flags是131
。
- 也就是堆中object和栈中object指向同一片内存空间。
三层拷贝总结
- block通过
_Block_copy
函数从栈拷贝到堆上。 - block捕获变量
Block_byref
,通过_Block_object_assign
函数从栈拷贝到堆上。 - Block_byref中的object也是通过_Block_object_assign函数进行相关拷贝。
注意:只有用__block
声明的对象才会有三层拷贝
block释放
释放时调用的__Block_byref_id_object_dispose_131
函数:
static void __Block_byref_id_object_dispose_131(void *src) {
_Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}
_Block_object_dispose
void _Block_object_dispose(const void *object, const int flags) {
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
case BLOCK_FIELD_IS_BYREF:
// get rid of the __block data structure held in a Block
_Block_byref_release(object);
break;
case BLOCK_FIELD_IS_BLOCK:
_Block_release(object);
break;
case BLOCK_FIELD_IS_OBJECT:
_Block_release_object(object);
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_WEAK:
break;
default:
break;
}
}
`