block是ios开发中很常见的内容,正确的使用block可以使代码更简洁,可读性更强。但是block也有自己的一些弊端,如果对代码不够熟悉,调试的时候就不是很方便,而且使用不当也会造成一些其他的问题,比如循环引用或者其他方式的内存泄漏等等。知己知彼,百战不殆,今天主要来探究一下block的相关实现。
探索block的内部结构
正确的block如下所示:
int age = 10;
void(^myBlock)(int, int) = ^(int a, int b) {
NSLog(@"age = %d", age);
NSLog(@"a = %d, b = %d", a, b);
};
myBlock(20, 30);
上述代码声明了一个名为myBlock的block,包含了两个int类型的参数,返回值为void,同时myBlock还捕获了一个外部变量age,当我们执行myBlock的时候,输入如下:
age = 10
a = 20, b = 30
通过命令行可以将上述代码转换成底层实现代码:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
转换成功后main.m旁边会多出一个main.cpp的文件,打开文件翻到最底部,可以看到转换完毕的对应代码:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int age = 10;
void(*myBlock)(int a, int b) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
((void (*)(__block_impl *, int, int))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock, 20, 30);
}
return 0;
}
通过上述代码可以发现,myBlock在c++代码中被转换成了一个__main_block_impl_0*类型的结构体指针。我们接下来看一下__main_block_impl_0结构体里面具体都有什么。
__main_block_impl_0
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
__main_block_impl_0内部成员变量有:
- 结构体
__block_impl - 结构体指针
__main_block_desc_0* - 捕获的外部变量
age - 一个构造方法
后面都会一一说到。
__block_impl
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
__block_impl有4个成员变量,我们通过lldb观察一下myBlock的结构如下图:
isa还可以确定的是myBlock确实是一个oc对象。
__main_block_impl_0构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
该函数接受了四个参数:
void *fp
在本例中即是:
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_zg_lr831ytd1ld6gjk2lt469nr80000gn_T_main_fb5446_mi_0, age);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_zg_lr831ytd1ld6gjk2lt469nr80000gn_T_main_fb5446_mi_1, a, b);
}
很显然,__main_block_func_0函数中存放的是我们定义的myBlock大括号里面的代码,该函数直接赋值给__main_block_impl_0构造函数的第一个参数fp指针,而fp又赋值给__block_impl结构体中的FuncPtr变量。
- __main_block_desc_0_DATA 参数
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)};
__main_block_desc_0_DATA是一个结构体,包含了__main_block_impl_0的内存大小,最终赋值给了__main_block_impl_0结构体类型变量Desc。
_age
直接赋值给了age,需要注意的是此处捕获了自动变量为值拷贝。flags
默认设置为0,在block的内部操作中会用到,记录了引用计数以及其他的一些特性
block的调用
((void (*)(__block_impl *, int, int))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock, 20, 30);
其中:
((void (*)(__block_impl *, int, int))为函数的类型((__block_impl *)myBlock)->FuncPtr),在上面我们已经知道myBlock其实对应的是__main_block_impl_0的结构体,但是这里进行了类型转换,转换成了__block_impl的结构体,进而拿到了对应的FuncPtr。这样做的原因是因为__main_block_impl_0中__block_impl位于结构体的第一位,因此他们的指针地址都指向了同一片内存空间,可以进行强制转换,至于为什么这样做而不是先获取__block_impl再获取FuncPtr,可能是因为直接进行类型转换在性能上会优越一点,毕竟少了一步获取__block_impl的过程。- 获取到
FuncPtr之后,因为FuncPtr对应的其实就是我们的__main_block_func_0,它需要接收三个参数分别是:block本身以及两个形参a和b。
由上分析上述代码可以简化为:
(__block_impl *)block->FuncPtr(block, 20, 30);
其实就是调用了myBlock中我们自己定义的实现。
Block访问(捕获)外部变量
int a = 10;
static int b = 20;
int main(int argc, const char * argv[]) {
@autoreleasepool {
auto int c = 30;
static int d = 40;
void(^myBlock)(void) = ^() {
NSLog(@"a=%d, b=%d, c=%d, d=%d", a, b, c, d);
};
a++;
b++;
c++;
d++;
myBlock();
}
return 0;
}
执行以上代码,结果如下:
a=11, b=21, c=30, d=41
发现除了自动变量c以外的全局变量a、静态全局变量b、静态变量d都变了。为什么呢?转换为底层代码来一探究竟。
int a = 10;
static int b = 20;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int c;
int *d;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _c, int *_d, int flags=0) : c(_c), d(_d) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int c = __cself->c; // bound by copy
int *d = __cself->d; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_zg_lr831ytd1ld6gjk2lt469nr80000gn_T_main_b7ba44_mi_0, a, b, c, (*d));
}
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 argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
auto int c = 30;
static int d = 40;
void(*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, c, &d));
a++;
b++;
c++;
d++;
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
}
return 0;
}
根据转换过来的c++源码,发现其中的结构体和函数之间的关系大致如下图所示:
局部变量/静态局部变量
我们先来看__main_block_impl_0结构体,和之前一样,局部变量c是以值拷贝的形式传到了结构体中,而静态变量d并不是值拷贝的形式被捕获,__main_block_impl_0结构体中保存了&d,即一个指向静态变量d的指针。
再看一下myBlock的实现代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int c = __cself->c; // bound by copy
int *d = __cself->d; // bound by copy
...
}
c是值拷贝的自动变量,d是值拷贝的指针,这两个有什么区别呢?区别就在于我们在block内部访问的值c和外部的自动变量c没有任何关系。而我们通过*d访问到的还是外部的静态变量d,虽然拷贝了一份指针,但是他们还是指向了同一份内存空间。因此执行了d++以后我们执行myBlock访问到了修改以后的静态变量d。
全局变量/静态全局变量
上面分析了c和d,接下来我们看一下全局变量a、b。
我们发现__main_block_impl_0内部并没有a、b这两个变量,但是在__main_block_func_0中直接就访问了这两个变量进行了输出。说到这里需要补充一下a、b、c、d这四种变量的声明周期和作用域。
全局变量a、全局静态变量b、静态变量d存放在全局区,并不会随时被销毁,其值一直会在内存中保持不变,直到整个程序结束时才销毁。局部变量c存放在栈区,因为作用域(或生命周期)有限,在作用域结束之后会被销毁,故block在引用时系统会自动将其值保存在block结构体中(即捕获)。
因此,block不需要捕获a、b,而是在使用的时候直接访问即可。静态变量d之所以拷贝了指针,是因为d属于局部静态变量,它的作用域在当前的函数内部,传递指针可以确保block在出了作用域之后仍然可以访问到d。
oc对象
main.m代码如下:
typedef void(^blk_t)(id obj);
int main(int argc, const char * argv[]) {
@autoreleasepool {
blk_t blk;
{
id array = [[NSMutableArray alloc]init];
blk = ^(id obj) {
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
};
}
blk([[NSObject alloc]init]);
blk([[NSObject alloc]init]);
blk([[NSObject alloc]init]);
}
return 0;
}
在array变量的作用于结束的同时,变量array被废弃,其强引用失效,因此赋值给变量array的NSMutableArray类的对象必定被释放并废弃。但是该源代码运行正常,结果如下:
array count = 1
array count = 2
array count = 3
这一结果意味着赋值给变量array的NSMutableArray类的对象在该源代码最后Block的执行部分超出其变量作用域而存在。通过以下命令将main.m转换为c++代码
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
先上关系图:
__main_block_impl_0
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
id array;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id _array, int flags=0) : array(_array) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
__main_block_impl_0其内部多了一个id类型的array
__main_block_desc_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};
发现__main_block_desc_0结构体中多了两个方法copy和dispose,这两个方法的参数都是和我们捕获的对象array有关。具体作用后面会讲到。
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}
__main_block_func_0
因为结构体__main_block_impl_0中的array其实就是一个指向array对象的指针,因此和外部栈上的array指向的是同一份内存空间,调用addObject方法是一样的,可以正常调用。
__block变量
除了以上几种,和block相关的还有一个__block类型的变量。__block修饰的变量被block捕获之后,在block的内部可以修改该变量的值,注意,修改的该变量本身的值。如果我们尝试修改非__block修饰的变量时,编译器会报一下错误:
同时,编译器也会提醒我们变量a缺少了__block修饰符。
typedef void(^blk_t)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int a = 10;
blk_t blk = ^{
a = 1;
};
blk();
NSLog(@"a=%d", a);
}
return 0;
}
修改为上述代码后,打印结果为:
a=1
变量a经过__block修饰之后就可以在block之中来进行修改了,接下来就来探索一下其可以修改的原因。
首先,还是通过命令将main.m进行转换。
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
typedef void(*blk_t)(void);
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
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
(a->__forwarding->a) = 1;
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
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};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
blk_t blk = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_zg_lr831ytd1ld6gjk2lt469nr80000gn_T_main_dfd779_mi_0, (a.__forwarding->a));
}
return 0;
}
转换过后代码变得好长,我们一点一点来看,还是先上图:
变量a经过__block修饰之后,结构发生了变化,已经不是一个简单的局部变量了,变成了一个__Block_byref_a_0类型的结构体。
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
在main函数中,该结构体初始化的代码为
__Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
我们可以发现结构体中变量a的值为初始化的值10,而__forwarding是一个指向结构体本身的指针。暂且先了解到这里,具体原因后面再讲。
__main_block_impl_0
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;
}
};
__main_block_impl_0里面也包含了一个__Block_byref_a_0*指针变量a,需要注意的是在结构体初始化方法中,指针a指向的地址并不是传进来的形参_a,而是_a->__forwarding。
在main函数中,该结构体初始化的代码为:
blk_t blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
__main_block_func_0
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
(a->__forwarding->a) = 1;
}
并不是直接修改结构体中int类型变量a的值,而是修改了a->__forwarding结构体中int类型变量a的值。
__main_block_desc_0
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
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};
__main_block_desc_0相关的代码和block访问对象时类似,唯一的区别是copy和dispose函数的最后一个参数由3变成了8,即从BLOCK_FIELD_IS_OBJECT变成了BLOCK_FIELD_IS_BYREF。关于copy和dispose相关的内容会在后面进行解释。
__block对象
__block BlockObject *blockObject = [BlockObject new];
blk_t blk1 = ^(){
NSLog(@"%@", blockObject);
};
转换之后:
struct __Block_byref_blockObject_0 {
void *__isa;
__Block_byref_blockObject_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
BlockObject *blockObject;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_blockObject_0 *blockObject; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_blockObject_0 *_blockObject, int flags=0) : blockObject(_blockObject->__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_blockObject_0 *blockObject = __cself->blockObject; // bound by ref
(blockObject->__forwarding->blockObject);
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->blockObject, (void*)src->blockObject, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->blockObject, 8/*BLOCK_FIELD_IS_BYREF*/);}
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};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_blockObject_0 blockObject = {(void*)0,(__Block_byref_blockObject_0 *)&blockObject, 33554432, sizeof(__Block_byref_blockObject_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((BlockObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("BlockObject"), sel_registerName("new"))};
blk_t blk1 = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_blockObject_0 *)&blockObject, 570425344));
struct Block_layout* block_layout1 = (__bridge struct Block_layout*)blk1;
}
return 0;
}
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
_Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}
上述代码看起来像是__block基本变量和block访问变量二者的结合体。区别在于__Block_byref_blockObject_0结构体内部增加了copy/dispose的函数指针。具体作用后面会讲到。
Block的存储域
在上一部分,我们分别分析了block访问外部变量的几种情况:
- 全局(包括全局静态)变量:直接访问
- 局部非静态变量:值拷贝
- 局部静态变量:拷贝指针
- 对象:拷贝指针
__block变量:转换为结构体,保存__forwarding指针
其中还留有一些疑问,主要包括:
- 上面几种情况下
C++源码中__main_block_impl_0的isa全部都是&_NSConcreteStackBlock,这个是什么意思?还有没有其他的值? __block的__forwarding指针有什么作用?为什么要这样设计?- 在
block捕获对象的时候,为什么array出了作用域还能够存在? __main_block_desc_0中的copy和dispose函数是干啥用的?
接下来我们来解答这些问题
isa
先看下面的这段代码:
typedef void(^blk)(void);
int a = 1;
blk blk0 = ^(){
NSLog(@"%d", a);
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
blk blk1 = ^{
NSLog(@"%d", a);
};
static int b = 2;
blk blk2 = ^{
NSLog(@"%d", b);
};
blk blk3 = ^{
};
int d = 4;
blk blk4 = ^{
NSLog(@"%d", d);
};
NSLog(@"blk0=%@", blk0);
NSLog(@"blk1=%@", blk1);
NSLog(@"blk2=%@", blk2);
NSLog(@"blk3=%@", blk3);
NSLog(@"blk4=%@", blk4);
int e = 5;
NSLog(@"blk5=%@", ^{e;});
NSLog(@"blk6=%@", ^{a;});
NSLog(@"blk7=%@", ^{});
}
return 0;
}
打印结果为:
blk0=<__NSGlobalBlock__: 0x100001030>
blk1=<__NSGlobalBlock__: 0x100001050>
blk2=<__NSGlobalBlock__: 0x100001070>
blk3=<__NSGlobalBlock__: 0x100001090>
blk4=<__NSMallocBlock__: 0x10061ecf0>
blk5=<__NSStackBlock__: 0x7ffeefbff448>
blk6=<__NSGlobalBlock__: 0x1000010d0>
blk7=<__NSGlobalBlock__: 0x1000010f0>
将上述结构进行分类如下所示:
| 捕获变量类型 | 声明位置 | 类型 |
|---|---|---|
| 全局变量 | 全局区域 | Global |
| 全局变量 | main内 | Global |
| 静态局部变量 | main内 | Global |
| 未捕获变量 | main内 | Global |
| 自动变量 | main内 | Malloc |
| 自动变量(直接打印) | main内 | Stack |
| 全局变量(直接打印) | main内 | Global |
| 未捕获变量(直接打印) | main内 | Global |
NSStackBlock
该类的对象Block设置在栈上,我们知道栈上的变量会随着作用域的结束而被释放。通过转换C++的源码可以发现,其实除了在全局区域声明的blk0的isa为&_NSConcreteGlobalBlock,其他的blk全部为_NSConcreteStackBlock,但是打印的结果来看只有直接打印的blk5才属于__NSStackBlock__,但是这种类型的block没有办法使用,因为没有一个变量来指向它,如果直接声明之后就调用这种情况又没有意义。为什么__NSStackBlock__最终会转换成__NSGlobalBlock__或者__NSMallocBlock__呢?
NSGlobalBlock
__NSGlobalBlock__顾名思义,和全局变量一样,该类的对象Block设置在程序的数据区域(.data区)中。
通过C++源码我们可以发现,当block捕获全局变量时,它的isa为&_NSConcreteGlobalBlock,这个没有问题。但是发现在main函数内部声明的blk1、blk2、blk3、blk6、blk7的isa为&_NSConcreteStackBlock,只不过最终打印出来的是__NSGlobalBlock__。也就是说,即使在函数内而不在记述广域变量的地方使用Block语法时,只要Block不截获自动变量,就可以将Block用结构体实例设置在程序的数据区域。
总结以下两种情况生成的block属于__NSGlobalBlock__
- 记述全局变量的地方有
Block语法时 Block语法的表达式中不使用应截获的自动变量时。
NSMallocBlock
配置在栈上的Block会随着所属的变量作用域的结束而被废弃,如果Block当中有__block变量,该变量同样也会随着作用域的结束而被废弃。这就是__NSMallocBlock__的由来,Blocks提供了将Block和__block变量从栈上复制到堆上的方法来解决这个问题。这样即使栈上Block的变量作用域结束,堆上的Block还可以继续存在。此时Block的isa将由_NSConcreteStackBlock变为_NSConcreteMallocBlock。
我们接着来看如下代码:
typedef void(^blk_t)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int a = 10;
NSLog(@"a的地址为%p", &a);
^{
a = 1;
NSLog(@"stack-a的地址为%p", &a);
}();
blk_t blk = ^{
a = 1;
NSLog(@"heap-a的地址为%p", &a);
};
blk();
NSLog(@"a的地址为%p", &a);
}
return 0;
}
打印输出为:
a的地址为0x7ffeefbff4b8
stack-a的地址为0x7ffeefbff4b8
heap-a的地址为0x1007049c8
a的地址为0x1007049c8
blk_t blk赋值之前的两次打印打印的是栈上的地址。
__Block_byref_a_0结构体a已经被复制到了堆上,因此打印出来的的是堆上的地址。此时,栈上的结构体a可以通过__forwarding指针访问到堆上的a,即使栈上的变量废弃了,堆中的变量依然存在,可以正常访问。
那么什么情况下,栈上的block会被复制到堆上呢?大多数情况下编译器会适当的进行判断,自动生成将Block从栈上复制到堆上的代码。
- 调用
Block的copy方法时 Block作为函数返回值返回时- 将
Block赋值给__strong修饰符id类型的变量或者Block类型的成员变量 - 在方法名中含有
usingBlock的Cocoa框架方法或者Grand central Dispatch的API中传递Block时。
不过此外的情况需要我们手动进行copy生成将block从栈上复制到堆上的代码,以下是一个需要我们手动复制Block的一个例子:
@implementation BlockObject
+ (NSMutableArray *)blockArray {
int val = 10;
return [[NSMutableArray alloc]initWithObjects:
[^{NSLog(@"blk0:%d", val);}copy],
[^{NSLog(@"blk1:%d", val);}copy], nil];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableArray *array = [BlockObject blockArray];
typedef void(^blk_t)(void);
blk_t blk0 = array[0];
blk_t blk1 = array[1];
blk0();
}
return 0;
}
如果不加copy会导致blk_t在出了作用域之后被废弃,出现野指针异常
Block源码剖析
接下来我们看一下关于Block的真正源码。
Block.private
通过苹果源码当中Block_private可以发现block的结构如下:
block结构体
struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
void (*invoke)(void *, ...);
struct Block_descriptor_1 *descriptor;
// imported variables
};
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
void (*copy)(void *dst, const void *src);
void (*dispose)(const void *);
};
struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
虽然Block_layout只有Block_descriptor_1,但是实际上会根据flags的值在Block_descriptor_1内存区域的后面追加Block_descriptor_2或者Block_descriptor_3,后面会对这一块进行分析。
接下来看一下和Block_layout->flags相关的定义。
// Values for Block_layout->flags to describe block objects
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime Block被释放
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime 引用计数掩码
BLOCK_NEEDS_FREE = (1 << 24), // runtime 标识堆block
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler 含有copy/dispose辅助函数
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code
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_layout的结构,我们怎么验证呢?其实也很简单,代码如下:
blk和block_layout是可以一一对应的,同时block_layout也持有一个object的指针,指向我们创建的object对象,这也从侧面验证了block容易造成循环引用。
-
关于
flags
细心的读者可以发现,flags的值是一个负数。这是什么原因呢?
falgs是一个4字节的int值,它的首位为1,所以在打印的时候系统认为第一位是符号位,被转成了一个负值。将诶下来我们验证一下falgs的值。
根据上面二进制的打印,可以看出,当前flags的组成为
BLOCK_NEEDS_FREE|BLOCK_HAS_COPY_DISPOSE|BLOCK_HAS_SIGNATURE|BLOCK_HAS_EXTENDED_LAYOUT
同时block的引用计数为1,和上上图中的内容相符合。
-
关于
Block_descriptor_2和Block_descriptor_3的取值
用图表示就是:
Block_descriptor_2和Block_descriptor_3并没有提供类似指针的方法可以获取,而是只能通过Block_descriptor_1的指针加上对应的偏移量来获取,因为Block_descriptor_2和Block_descriptor_3并不是一定会存在的。
__block结构体
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->flags的定义如下:
// Values for Block_byref->flags to describe __block variables
enum {
// Byref refcount must use the same bits as Block_layout refcount
// 引用计数必须和block_layout的引用计数用同样的位数
// BLOCK_DEALLOCATING = (0x0001), // runtime
// BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
BLOCK_BYREF_LAYOUT_MASK = (0xf << 28), // compiler
BLOCK_BYREF_LAYOUT_EXTENDED = ( 1 << 28), // compiler
BLOCK_BYREF_LAYOUT_NON_OBJECT = ( 2 << 28), // compiler
BLOCK_BYREF_LAYOUT_STRONG = ( 3 << 28), // compiler
BLOCK_BYREF_LAYOUT_WEAK = ( 4 << 28), // compiler
BLOCK_BYREF_LAYOUT_UNRETAINED = ( 5 << 28), // compiler
BLOCK_BYREF_IS_GC = ( 1 << 27), // runtime
BLOCK_BYREF_HAS_COPY_DISPOSE = ( 1 << 25), // compiler
BLOCK_BYREF_NEEDS_FREE = ( 1 << 24), // runtime
};
runtime.cpp
接下来我们看一下相关的具体方法实现。
首先我们需要注意的是几个枚举值
/ Runtime support functions used by compiler when generating copy/dispose helpers
// Values for _Block_object_assign() and _Block_object_dispose() parameters
enum {
// see function implementation for a more complete description of these fields and combinations
BLOCK_FIELD_IS_OBJECT = 3, // id, NSObject, __attribute__((NSObject)), block, ... oc对象类型
BLOCK_FIELD_IS_BLOCK = 7, // a block variable 另一个block
BLOCK_FIELD_IS_BYREF = 8, // the on stack structure holding the __block variable __block类型
BLOCK_FIELD_IS_WEAK = 16, // declared __weak, only used in byref copy helpers __weak类型
BLOCK_BYREF_CALLER = 128, // called from __block (byref) copy/dispose support routines. __block结构体标识
};
enum {
BLOCK_ALL_COPY_DISPOSE_FLAGS =
BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_BYREF |
BLOCK_FIELD_IS_WEAK | BLOCK_BYREF_CALLER
};
和block内存相关的方法有以下几个:
// Create a heap based copy of a Block or simply add a reference to an existing one.
// This must be paired with Block_release to recover memory, even when running
// under Objective-C Garbage Collection.
//基于copy在堆上创建一个Block或者只是单纯的增加一个已经存在的Block的引用计数,必须和Block_release配对使用来还原内存。
BLOCK_EXPORT void *_Block_copy(const void *aBlock)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
// Lose the reference, and if heap based and last reference, recover the memory
// 减少引用计数,如果是堆上的最后一个引用,恢复内存
BLOCK_EXPORT void _Block_release(const void *aBlock)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
// Used by the compiler. Do not call this function yourself.
//辅助函数,编译器使用,不要手动调起
BLOCK_EXPORT void _Block_object_assign(void *, const void *, const int)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
// Used by the compiler. Do not call this function yourself.
//辅助函数,编译器使用,不要手动调起
BLOCK_EXPORT void _Block_object_dispose(const void *, const int)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
_Block_copy
// Copy, or bump refcount, of a block. If really copying, call the copy helper if present.
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;
//如果是堆上的block,增加引用计数
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;
}
else {
// Its a stack block. Make a copy.
// block在栈上,需要copy到堆上
// 1. 在堆上开辟同等大小的内存空间
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
if (!result) return NULL;
// 2. 从栈上的block复制同等大小的字节内容到新开辟的堆内存中
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
// Resign the invoke pointer as it uses address authentication.
// 3. 堆上block的invoke指针指向正确的位置
result->invoke = aBlock->invoke;
#endif
// reset refcount
// 4. 重置引用计数, BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING刚好是0xffff,取反则变成0x0000
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
// 5. 引用计数置为1, 因为flags第1位存放的是BLOCK_DEALLOCATING,因此真实的引用计数从第2位开始。 2可以看做是二进制的10
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
// 6. 调用辅助函数
_Block_call_copy_helper(result, aBlock);
// Set isa last so memory analysis tools see a fully-initialized object.
// 7. 更正isa的值为malloc block
result->isa = _NSConcreteMallocBlock;
return result;
}
}
latching_incr_int
static int32_t latching_incr_int(volatile int32_t *where) {
while (1) {
//旧的flags值
int32_t old_value = *where;
//如果与上掩码得到的值和掩码一样,说明引用计数达到最大,直接返回。这种情况基本不会出现。
if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
return BLOCK_REFCOUNT_MASK;
}
//引用计数+1,因为是从第二位开始,因此是以2为单位,引用计数的+1表现为数据上的+2
if (OSAtomicCompareAndSwapInt(old_value, old_value+2, where)) {
return old_value+2;
}
}
}
_Block_object_assign
_Block_call_copy_helper函数最终会调用_Block_object_assign函数,该函数根据不同的flags值,在内部进行判断,执行了不同的操作。
实现如下:
//
// When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point
// to do the assignment. 当Blocks或者Block_byrefs持有objects时他们的copy辅助会用这个方法来进行赋值
//
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:
/*******
id object = ...;
[^{ object; } copy];
********/
//该方法为空方法
_Block_retain_object(object);
//一个新的指针指向object,此时object的引用计数就会增加
*dest = object;
break;
//block内部捕获了另一个block
case BLOCK_FIELD_IS_BLOCK:
/*******
void (^object)(void) = ...;
[^{ object; } copy];
********/
*dest = _Block_copy(object);
break;
//block内部捕获了__block变量或者__weak __block变量,此时需要拷贝block_byref结构体到堆上
case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
case BLOCK_FIELD_IS_BYREF:
/*******
// copy the onstack __block container to the heap
// Note this __weak is old GC-weak/MRC-unretained.
// ARC-style __weak is handled by the copy helper directly.
__block ... x;
__weak __block ... x;
[^{ x; } copy];
********/
*dest = _Block_byref_copy(object);
break;
//以下为block_byref结构体拷贝过程中,内部的变量类型的处理,会在_Block_byref_copy方法内部调起
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
/*******
// copy the actual field held in the __block container
// Note this is MRC unretained __block only.
// ARC retained __block is handled by the copy helper directly.
__block id object;
__block void (^object)(void);
[^{ object; } copy];
********/
*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:
/*******
// copy the actual field held in the __block container
// Note this __weak is old GC-weak/MRC-unretained.
// ARC-style __weak is handled by the copy helper directly.
__weak __block id object;
__weak __block void (^object)(void);
[^{ object; } copy];
********/
*dest = object;
break;
default:
break;
}
}
_Block_object_assign使用到的场景主要有以下几个:
block语法内部捕获了oc对象、__block变量,将block从栈上拷贝到堆上时__block变量为oc对象,Block_byref结构体从栈上拷贝到堆上时
_Block_byref_copy
static struct Block_byref *_Block_byref_copy(const void *arg) {
//src表示栈上的Block_byref结构体
struct Block_byref *src = (struct Block_byref *)arg;
//引用计数为0
if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
// src points to stack
// 在堆上开辟新的空间
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
// isa指针置为null,Block_byref结构体不是对象。
copy->isa = NULL;
// byref value 4 is logical refcount of 2: one for caller, one for stack
// 标识为堆结构体并且引用计数为2,一个为src->forwading, 一个为copy->forwading
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
// 通过地址偏移拿到Block_byref_2结构体
struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
//keep/destroy函数的最终实现也是_Block_object_assign/_Block_object_dispose,只不过传递的参数为Block_byref结构体,需要通过地址偏移来拿到对应的对象。如下:
// static void __Block_byref_id_object_copy_131(void *dst, void *src) {
// _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
// }
// static void __Block_byref_id_object_dispose_131(void *src) {
// _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
// }
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.
// 从copy的下一位开始,放入Block_byref_3,可以发现src-size并不是Block_byref结构体的大小,而是整体的大小
memmove(copy+1, src+1, src->size - sizeof(*src));
}
}
// already copied to heap
// 引用计数不为0,表示已经复制到堆上了,增加引用计数
else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
latching_incr_int(&src->forwarding->flags);
}
return src->forwarding;
}
到此为止,copy相关的内容已经结束了,接下来看一下release相关的。
_Block_release
// API entry point to release a copied Block
void _Block_release(const void *arg) {
struct Block_layout *aBlock = (struct Block_layout *)arg;
if (!aBlock) return;
//全局block不需要处理
if (aBlock->flags & BLOCK_IS_GLOBAL) return;
//栈上的block不需要处理
if (! (aBlock->flags & BLOCK_NEEDS_FREE)) return;
//判断需不需要释放
if (latching_decr_int_should_deallocate(&aBlock->flags)) {
//调用release的辅助函数
_Block_call_dispose_helper(aBlock);
//销毁block
_Block_destructInstance(aBlock);
//释放内存
free(aBlock);
}
}
接下来看一下判断block是否需要释放的函数
latching_decr_int_should_deallocate
// return should_deallocate?
static bool latching_decr_int_should_deallocate(volatile int32_t *where) {
while (1) {
int32_t old_value = *where;
// 引用计数最大
if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
return false; // latched high
}
// 引用计数为0
if ((old_value & BLOCK_REFCOUNT_MASK) == 0) {
return false; // underflow, latch low
}
// 引用计数-1,之前说过引用计数是从第2位开始,第一位为是否在释放的标识。因此需要-2
int32_t new_value = old_value - 2;
bool result = false;
//如果引用计数刚好为1。二进制为10,即2
if ((old_value & (BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING)) == 2) {
//-1会导致第一位为1,即BLOCK_DEALLOCATING为1,标识在释放了
new_value = old_value - 1;
result = true;
}
//赋值
if (OSAtomicCompareAndSwapInt(old_value, new_value, where)) {
return result;
}
}
}
接下来看_Block_call_dispose_helper
_Block_call_dispose_helper
// When Blocks or Block_byrefs hold objects their destroy helper routines call this entry point
// to help dispose of the contents
void _Block_object_dispose(const void *object, const int flags) {
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
// __block需要释放Block_byref结构体
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;
// 捕获的block需要被释放
case BLOCK_FIELD_IS_BLOCK:
_Block_release(object);
break;
// 捕获的对象需要调用release
case BLOCK_FIELD_IS_OBJECT:
//实现为空,block释放的时候,指向object的指针被释放,因此object引用计数也会减少
_Block_release_object(object);
break;
// 堆上的Block_byref不做处理,在上面BLOCK_FIELD_IS_BYREF的case里面已经处理过了,Block_byref结构体释放后,相关指针也会被释放,对应的引用计数会自然-1,相关的对象也会被释放。
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_byref_release
static void _Block_byref_release(const void *arg) {
struct Block_byref *byref = (struct Block_byref *)arg;
// dereference the forwarding pointer since the compiler isn‘t doing this anymore (ever?)
byref = byref->forwarding;
//如果是Block_byref结构体在堆上
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);
}
}
}