iOS-block(二)-底层分析
前言
上一篇我们讲完了block
的基础知识,这一篇我们就来看看block
的底层原理。话不多说,我们创建一个testBlock.c
的文件,输入以下代码:
#include "stdio.h"
int main() {
int a = 5;
void(^block)(void) = ^{
printf("BLOCK_TEST==%d", a);
};
block();
return 0;
}
然后对代码编译成.cpp
文件,此时main
函数就变成下面的样子:
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
printf("BLOCK_TEST==%d", a);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(){
int a = 5;
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);
return 0;
}
使用clang
指令的时候,需要注意不要使用字面量。
block
分析
在对代码的分析之前我们先抛出几个问题:
-
block
的本质到底是什么 -
block
为什么需要调用block()
-
block
是如何截获外界变量的 -
__block
的实现
block
本质
在Objective-C
中,block
是一个对象,从编译的结果来看,block
在本质上还是一个结构体struct
。而我们通常所说的block
是一个匿名函数也能提现出来,比如例子中,系统默认给block
分配了一个函数名称__main_block_impl_0
,其参数分别是(void *)__main_block_func_0
、__main_block_desc_0_DATA
、a
;
调用block()
而block
的声明是:
((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a))
实现则是:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy 值拷贝
printf("BLOCK_TEST==%d", a);
}
使用函数就需要调用,block
的最终执行调用就是block->FuncPtr(block)
。
捕获外界变量
通过编译结果,可以看到__main_block_impl_0
这个结构体有一个int a
的元素,而结构体内部的__main_block_impl_0
函数有: a(_a)
这么一个赋值过程。我们可以把源代码中int a
相关的代码删除,重新编译,会发现得到的结果中并没有相关的参数和赋值过程。这说明block
捕获外界变量的时候是自己创建一个同名变量将其进行复制操作,赋值之后,block
内部的变量已经和外部变量没有了关系。所以我们在《block
(一)-初探》中有一个例子:
typedef int(^myBlock)(int a, int b);
int d = 10;
myBlock mb = ^int(int a, int b) {
return a + b + d;
};
d = 5;
NSLog(@"==myBlock==%d==", mb(1, 2)); // 13
而且我们可以看到,在block
调用函数的时候,还会执行一次创建临时变量赋值的操作。int a = __cself->a
,这又不是同一个变量。所以当我们在block
内部直接对外部的变量进行操作(赋值)的时候,其实操作的内部的同名临时变量,而不是外部的变量。所以block
无法直接给外部截获的变量赋值,因为它在自己内部生成了一个同名临时变量,所有的操作都是内部的临时变量。那么我们需要在block
内部处理截获的外部变量该怎么办呢?答案是使用__block
。
block
源码
通过源码我们可以看出block
在底层的结构如下:
struct Block_layout {
void *isa; // isa指向
volatile int32_t flags; // contains ref count 标志状态
int32_t reserved;
BlockInvokeFunction invoke; // 函数执行
struct Block_descriptor_1 *descriptor; // block的附加描述信息 如size等
// imported variables
};
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};
flags
是block
的状态标志位。含义如下:
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime 标记正在释放
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime 存储引用计数的值
BLOCK_NEEDS_FREE = (1 << 24), // runtime 是否增加或减少引用计数的值
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler 是否拥有拷贝辅助函数 确定block是否存在Block_descriptor_2这个参数
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code 是否有C++析构函数
BLOCK_IS_GC = (1 << 27), // runtime 是否有垃圾回收
BLOCK_IS_GLOBAL = (1 << 28), // compiler 是否是全局block
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler 是否拥有签名
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler 确定Block_descriptor_3中的layout参数
};
另外还有两个可选的参数:
#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
需要flags
是BLOCK_HAS_COPY_DISPOSE
才可以,而Block_descriptor_3
需要flags
是BLOCK_HAS_SIGNATURE
。
了解了block
的内部结构,我们再来看看block
从全局block
、栈block
到堆block
是怎么变化的。
首先,我们实现一个全局block
:
void(^myBlock)(void) = ^ {
NSLog(@"==myBlock==");
};
在block
处设置一个断点,进入汇编,objc_retainBlock
、我们在此时读一下寄存器,继续跳转会进入,_Block_copy
,在_Block_copy
的return
处读一下寄存器。结果如下图所示:
这说明调用_Block_copy
使得栈block
变成了堆block
。下面我们来看看其源码:
// 传入的对象
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;
if (!arg) return NULL;
aBlock = (struct Block_layout *)arg;
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// 如果需要对引用计数进行处理,那就直接处理,处理完就返回
// block的引用计数是不由runtime下层处理,需要自己处理
// 这个地方处理的是堆区block
latching_incr_int(&aBlock->flags);
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
// 如果是全局block 直接返回
return aBlock;
}
else {
// Its a stack block. Make a copy
// 栈区block 使用copy
// 先在堆区初始化一块内存空间
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
if (!result) return NULL;
// 将栈区的数据copy到堆区的空间
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
// Resign the invoke pointer as it uses address authentication.
result->invoke = aBlock->invoke;
#endif
// 设置标志位
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);
// 设置为_NSConcreteMallocBlock
result->isa = _NSConcreteMallocBlock;
return result;
}
}
// 处理引用计数
static int32_t latching_incr_int(volatile int32_t *where) {
while (1) {
int32_t old_value = *where;
if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
return BLOCK_REFCOUNT_MASK;
}
if (OSAtomicCompareAndSwapInt(old_value, old_value+2, where)) {
return old_value+2;
}
}
}
从代码,我们可以看出block
的copy
动作主要如下:
- 全局
block
不做任何事情直接返回 - 堆区
block
增加引用计数然后返回 - 栈区
block
- 在堆区申请一块内存空间
- 将栈区的数据拷贝到堆区申请的空间
- 给相关标志位赋值,对
Block_descriptor_2
做copy
动作,将isa
设置为_NSConcreteMallocBlock
block
的引用计数是不由runtime
下层处理,需要自己处理。
__block
下面我们再根据编译的代码来看看__block
到底做了什么。先把下面代码进行编译:
int main(int argc, char * argv[]) {
@autoreleasepool {
__block NSString *name = [NSString stringWithFormat:@"%@", @"AAA"];
void(^myBlock)(void) = ^{
name = @"BBB";
NSLog(@"==name==%@==", name);
};
myBlock();
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
我们先来看看mian
函数编译之后的代码:
int main(int argc, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_name_0 name = {
(void*)0,
(__Block_byref_name_0 *)&name,
33554432,
sizeof(__Block_byref_name_0),
__Block_byref_id_object_copy_131,
__Block_byref_id_object_dispose_131,
((NSString * _Nonnull (*)(id, SEL, NSString * _Nonnull, ...))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("stringWithFormat:"), (NSString *)&__NSConstantStringImpl__var_folders_nw_tqjtztpn1yq6w0_wmgdvn_vc0000gn_T_main_41740c_mi_0, (NSString *)&__NSConstantStringImpl__var_folders_nw_tqjtztpn1yq6w0_wmgdvn_vc0000gn_T_main_41740c_mi_1)
};
void(*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_name_0 *)&name, 570425344));
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
return UIApplicationMain(argc, argv, __null, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class"))));
}
}
__block
修饰的name
对象被转化成了一个__Block_byref_name_0
的结构体,其源码如下:
struct Block_byref {
void *isa;
struct Block_byref *forwarding;
volatile int32_t flags; // contains ref count
uint32_t size;
};
struct Block_byref_2 {
// requires BLOCK_BYREF_HAS_COPY_DISPOSE
BlockByrefKeepFunction byref_keep;
BlockByrefDestroyFunction byref_destroy;
};
struct Block_byref_3 {
// requires BLOCK_BYREF_LAYOUT_EXTENDED
const char *layout;
};
可以看出,__Block_byref_name_0
中的(void*)0
就是Block_byref
的void *isa
,(__Block_byref_name_0 *)&name
即为struct Block_byref *forwarding
,__Block_byref_id_object_copy_131
即为BlockByrefKeepFunction byref_keep
函数。
上面我们讲述了block
从栈到堆的拷贝过程。下面再来看看__Block_byref_name_0
的拷贝动作。在block
里我们传入了一个&__main_block_desc_0_DATA
的结构体地址,该结构体在初始化的时候传入了__main_block_copy_0
方法进行拷贝操作,实现如下:
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->name, (void*)src->name, 8/*BLOCK_FIELD_IS_BYREF*/);
}
enum {
BLOCK_FIELD_IS_OBJECT = 3, // 截获的是对象 __attribute__((NSObject)), block, ...
BLOCK_FIELD_IS_BLOCK = 7, // 截获的是block变量
BLOCK_FIELD_IS_BYREF = 8, // 截获的是__block修饰的对象
BLOCK_FIELD_IS_WEAK = 16, // 截获的是__weak修饰的对象
BLOCK_BYREF_CALLER = 128, // called from __block (byref) copy/dispose support routines.
};
// 根据传入的对象的类型
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
或block_byrefs
截获的对象的类型进行不同的内存管理处理:
- 截获的变量是对象,只需要赋值,引用计数不做任何处理,因为对象的引用计数是
runtime
底层自己处理的。
- 截获的变量是对象,只需要赋值,引用计数不做任何处理,因为对象的引用计数是
static void (*_Block_retain_object)(const void *ptr) = _Block_retain_object_default;
static void _Block_retain_object_default(const void *ptr __unused) { }
- 如果截获的变量是
block
对象,调用_Block_copy
方法。
- 如果截获的变量是
void *_Block_copy(const void *arg) {
// 详见上面的分析
}
- 如果截获的变量是
__block
对象,需要重新申请一块堆内存,然后将截获的对象也就是上述例子中的__Block_byref_name_0
结构体赋值给新的结构体,并将它们的forwarding
指针都指向新生成的结构体。其实也就是对__block
修饰的对象做了一次拷贝动作,然后让他们都指向同一块内存区域达到修改其中一个两个都改变的目的。
static struct Block_byref *_Block_byref_copy(const void *arg) {
// 创建一个临时变量
struct Block_byref *src = (struct Block_byref *)arg;
if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
// src points to stack
// 1.申请堆内存空间
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
// 2. 给新申请的空间赋值
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的对象和源对象都指向堆内存的拷贝地址
copy->forwarding = copy; // 堆拷贝指向自己
src->forwarding = copy; // 栈拷贝指向堆内存
copy->size = src->size;
if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
// 处理desc2 内存偏移取值
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) {
// 处理desc3
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 {
memmove(copy+1, src+1, src->size - sizeof(*src));
}
} else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
latching_incr_int(&src->forwarding->flags);
}
return src->forwarding;
}
在新生成的结构体__Block_byref_name_0
中,还有一个名为__Block_byref_id_object_copy_131
的方法,该方法的实现如下:
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
该方法依然是调用了_Block_object_assign
方法,不过是传递的参数不同,是目标对象偏移了40字节,由于此时目标对象就是__Block_byref_name_0
,偏移40字节正好是NSString *name
。也就是说此时又是对name
这个对象的内存地址做了一次拷贝。
struct __Block_byref_name_0 {
void *__isa; // 8
__Block_byref_name_0 *__forwarding; // 8
int __flags; // 4
int __size; // 4
void (*__Block_byref_id_object_copy)(void*, void*); // 8
void (*__Block_byref_id_object_dispose)(void*); // 8
NSString *name;
};
所以,我们可以得出结论:__block
修饰的外部变量,在block
内部能够修改的主要原因在于3次拷贝。
-
block
的拷贝,从栈内存到堆内存。
-
- 对新生成的结构体的拷贝。
__block
修饰的变量会生成一个名为__Block_byref_XXX_0
结构体,将原来的进行了封装,然后把整个结构体地址指针传入block
内部。
- 对新生成的结构体的拷贝。
- 对原来的对象的内存的拷贝。
block
的释放
上面我们讲过了block
的持有过程,既然有持有,那就肯定有释放。下面我们来看看block
的释放过程。
在&__main_block_desc_0_DATA
的定义中,会传入__main_block_dispose_0
这样一个函数与void (*dispose)(struct __main_block_impl_0*)
方法相对应。其函数实现如下:
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->name, 8/*BLOCK_FIELD_IS_BYREF*/);
}
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:
_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;
}
}
- 需要释放的如果是
__block
修饰的对象,判断如果应该释放,则调用方法销毁创建的__block
结构。
- 需要释放的如果是
static void _Block_byref_release(const void *arg) {
struct Block_byref *byref = (struct Block_byref *)arg;
byref = byref->forwarding;
if (byref->flags & BLOCK_BYREF_NEEDS_FREE) {
int32_t refcount = byref->flags & BLOCK_REFCOUNT_MASK;
os_assert(refcount);
if (latching_decr_int_should_deallocate(&byref->flags)) {
if (byref->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
struct Block_byref_2 *byref2 = (struct Block_byref_2 *)(byref+1);
(*byref2->byref_destroy)(byref);
}
free(byref);
}
}
}
- 需要释放的如果是
block
,判断是否该释放,如果应该释放则调用释放block
的方法。
- 需要释放的如果是
void _Block_release(const void *arg) {
struct Block_layout *aBlock = (struct Block_layout *)arg;
if (!aBlock) return;
if (aBlock->flags & BLOCK_IS_GLOBAL) return;
if (! (aBlock->flags & BLOCK_NEEDS_FREE)) return;
if (latching_decr_int_should_deallocate(&aBlock->flags)) {
_Block_call_dispose_helper(aBlock);
_Block_destructInstance(aBlock);
free(aBlock);
}
}
- 需要释放的如果是对象,则什么都不用做,
ARC
下runtime
底层自己处理。
- 需要释放的如果是对象,则什么都不用做,
static void (*_Block_release_object)(const void *ptr) = _Block_release_object_default;
static void _Block_release_object_default(const void *ptr __unused) { }
block
的签名
我们在讲述block
的本质的时候说了,block
是匿名函数,那么作为一个函数,block
是否也有自己的签名。答案是肯定,上面我们的打印结果也体现出来了。
我们在讲述block
的源码中提到block
有两个可选的参数Block_descriptor_2
和Block_descriptor_3
。而block
的签名信息就放在Block_descriptor_3
中,一个名为signature
的元素。
struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
从上面的打印结果可以看出block
的签名样子如下:
signature: "v8@?0"
其中v
表示返回值是void
,@?
表示未知的对象,即为block
。这和方法签名是有所不同的,方法签名一般是v@:
这样的形式(此处只说返回值为void
的场景),:
表示SEL
。
总结
-
block
的本质是一个匿名函数,也是一个对象,其底层实现是结构体。 -
block
既有声明,也有调用。 -
block
也有签名,和方法签名略有不同。 -
block
截获外部变量是在定义的时候生成一个同名的中间变量,该变量的初值就是外部变量在被截获的时候的值,之后就与外部变量没有关系。 -
__block
修饰的外部变量在block
内部能够修改的原因在于3次拷贝:-
block
的拷贝,从栈内存到堆内存 - 将修饰的对象转化为一个结构体,将其拷贝到堆内存。
- 将修饰的对象的内存地址也进行了拷贝用以修改。
-
参考文献:
苹果官方文档
《Objective-C
高级编程iOS
和OS X
多线程和内存管理》