三年前,第一次写关于 block 的东西,就是初识 block,了解了些皮毛,但发现,那么仅仅是 block 的冰山一角,关于 block 还有很多需要参透和理解。
block 本质
block 的本质是一个 Objective-C 对象,其内部也有 isa 指针,block 中封装了函数的调用以及函数调用环境的 Objective-C 对象。它的结构如下:
- 函数的调用相当于函数的调用地址
- 函数调用环境指参数,访问 block 外部的值等
一段下面的 block:
void(^block)(int a, int b) = ^(int a, int b) {
NSLog(@"a + b = %d", a + b);
};
block(1, 2);
用命令 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
(下同) 重写后 C++ 代码是这样的:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
};
__block_impl
的声明:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
__main_block_desc_0
的声明:
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
};
Block_size
表示__main_block_impl_0
能占多少内存。
假如 block 内使用了外部变量,如:
int outter = 35;
void(^block)(int a, int b) = ^(int a, int b) {
NSLog(@"outter is %d", outter);
NSLog(@"a + b = %d", a + b);
};
block(1, 2);
本质结构为:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int outter;
};
若我们在 .m
文件中自行实现这些结构体:
[图片上传失败...(image-e5da00-1555345050037)]
然后进行转换:
struct __main_block_impl_0* blockStruct = (__bridge struct __main_block_impl_0*)block;
加断点运行后进入 LLDB 调试环境可看到 blockStruct 的信息:
发现 outter
已经封装到 blockStruct 的内存中去了。
我们记录下 __FuncPtr
后面的内存地址 0x0000000100000ee0,然后在 block 块内增加断点并过掉当前断点,当程序停留在 block 块内的断点的时候,然后 Debug -> Debug Workflow -> Always Show Disassembly 会看到如下界面:
[图片上传失败...(image-fe45c2-1555345050037)]
第一行 0x100000ee0 <+0>: pushq %rbp
的地址就是__FuncPtr
的地址,这说明 block 块内的代码都封装到了函数里面,这个函数的首地址(例子中的 0x0000000100000ee0)在 block 结构体的成员结构体 __block_impl 中。
深入探究
底层数据结构
在 main
函数中例子的代码 C++ 的实现为:
int outter = 35;
// 定义 block 变量
void(*block)(int a, int b) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, outter));
// 执行 block 内部代码
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 1, 2);
去除强制转换的干扰代码,简化后:
int outter = 35;
// 定义 block 变量
void(*block)(int a, int b) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, outter));
// 执行 block 内部代码
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 1, 2);
这里的 block
会指向什么?首先得明白 _main_block_impl_0()
会返回什么?我们在 .cpp
文件中发现该函数在 __main_block_impl_0
的结构体中:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int outter;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _outter, int flags=0) : outter(_outter) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
该函数接收 4 个参数,flags
默认为 0,并且函数名和结构体名相同,是 C++ 中的构造函数,和 Java 的构造函数道理类似,也和 Objective-C 中的 init
方法类似,并且无任何返回。
outter(_outter)
表示传进来的 _outter 的值会赋给结构体成员变量 outter,相当于outter = _outter
。
4 个传入的参数中 outter 不必多言,那么来 __main_block_func_0
是什么:
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
int outter = __cself->outter;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_33_1p51hcyn1738b6zr050n01qh0000gn_T_main_f61ac8_mi_0, outter);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_33_1p51hcyn1738b6zr050n01qh0000gn_T_main_f61ac8_mi_1, a + b);
}
可见,__main_block_func_0
封装了 block 执行逻辑的函数。__main_block_func_0 对应 __main_block_impl_0
构造方法中的 void *fp
, fp 赋值给了 impl.FuncPtr
。这样 impl.FuncPtr 存储的就是执行逻辑的函数的地址。
在该构造方法中同时初始化了 isa 指针:
impl.isa = &_NSConcreteStackBlock;
说明 block 的类型为 _NSConcreteStackBlock
。
回过头我们再看传入的第二个参数 &__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)};
该处 0 赋值给了 reserved
,sizeof(struct __main_block_impl_0)
计算该 block 结构体的大小并将结果赋值给 Block_size
。
&__main_block_desc_0_DATA
对应 __main_block_impl_0
构造方法中的 struct __main_block_desc_0 *desc
, 并赋值给了 Desc
。换而言之 __main_block_impl_0 中的 Desc 指向的是 __main_block_desc_0 结构体变量。
所以在执行结构体的构造函数的时候,outter 为 35。倘若在外部将 outter 重新赋值,结构体中的 outter 是不会更改的。也就是说 outter 是以值传递的形式传递的。
__main_block_func_0
中:
int outter = __cself->outter;
该步骤为取出 outter 的值(35)。
block 的变量捕获
为确保 block 能正确访问外部变量,block 有变量捕获机制,如下图:
auto: 局部变量默认是 auto 修饰的:
int a = 0;
等价于auto int a = 0;
,它表示自动变量,离开作用域后自动销毁。
那么 block 中的捕获是什么意思?就是 block 内部会新增一个成员变量用来存储外部变量的值,这个过程为捕获。
auto 修饰的变量
上一节例子中的 int 型 outter 就是自动变量,默认 auto 修饰。其访问方式是值传递。
static 修饰的变量
我们添加静态变量 outter2:
int outter = 35;
static int outter2 = 1210;
void(^block)(int a, int b) = ^(int a, int b) {
NSLog(@"outter is %d", outter);
NSLog(@"outter2 is %d", outter2);
NSLog(@"a + b = %d", a + b);
};
block(10, 20);
运行后打印了 outter 和 outter2 的值。说明无论是 auto 修饰还是 static 修饰的外部变量,block 内部都是能捕获到的。
那么内部访问方式是否一样?重写 C++ 代码后发现:
void(*block)(int a, int b) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, outter, &outter2));
outter2 是以 &outter2
传入 __main_block_impl_0
结构体的构造方法的,并且 __main_block_impl_0 中的 outter2 是:
struct __main_block_impl_0 {
...
int outter;
int *outter2;
...
};
发现这里的 outter2 是通过传址的方式传进去的,在打印的 C++ 实现中 outter2 是这样取值的:
NSLog((NSString *)&__NSConstantStringImpl__var_folders_33_1p51hcyn1738b6zr050n01qh0000gn_T_main_e5faae_mi_1, (*outter2));
*outter2
这样的取值方式是直接取出外面静态变量内存里的值。
Q:为什么会有这样的差异?
A:因为 auto 修饰的变量是可能自动销毁的,而 block 执行的时机未定,所以存在 block 执行内部代码的时候变量已经销毁的情况,这会导致程序的 Crash,所以外部变量需进行值传递。而 static 修饰的变量会一直存在于内存当中,不存在 block 执行的时候变量已经销毁的情况。
全局变量
我们验证全局变量的捕获机制,添加一个全局的成员变量 outter3:
int static outter3 = 1314;
并在内部打印,发现打印 1314,若在执行 block 之前修改 outter3 的值:
void(^block)(int a, int b) = ^(int a, int b) {
NSLog(@"outter is %d", outter);
NSLog(@"outter2 is %d", outter2);
NSLog(@"outter3 is %d", outter3);
NSLog(@"a + b = %d", a + b);
};
outter3 = 999;
block(10, 20);
打印得 outter3 = 999,看似和局部静态变量的道理一样,我们看下 C++ 实现得 __main_block_impl_0 结构体:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int outter;
int *outter2;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _outter, int *_outter2, int flags=0) : outter(_outter), outter2(_outter2) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
发现并无 outter3,在 block 内部打印的地方为:
NSLog((NSString *)&__NSConstantStringImpl__var_folders_33_1p51hcyn1738b6zr050n01qh0000gn_T_main_147262_mi_2, outter3);
也就是说,全局变量并没有捕获到 block 内部,而且,内部访问全局变量是直接访问的。
那么假如 block 中是如何捕获 self 的呢?
我们新建 Test
类:
.h:
@interface Test : NSObject
@property(nonatomic, copy) NSString* param;
- (void)test;
- (instancetype)initWithParam:(NSString*)param;
@end
.m:
@implementation Test
- (void)test {
void(^block)(void) = ^{
NSLog(@"====>%p", self);
};
block();
}
- (instancetype)initWithParam:(NSString*)param
{
self = [super init];
if (self) {
self.param = param;
}
return self;
}
@end
外部调用 test()
方法便可执行 block,并访问内部的 self。打印:
====>0x10070be20
重写 Test.m 文件后发现其 block 结构为:
struct __Test__test_block_impl_0 {
struct __block_impl impl;
struct __Test__test_block_desc_0* Desc;
Test *self;
__Test__test_block_impl_0(void *fp, struct __Test__test_block_desc_0 *desc, Test *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
发现 self 是通过指针传递进来的。而且可推导,既然能捕获,说明 self 是局部变量。
我们可看到 test() 函数的底层为:
static void _I_Test_test(Test * self, SEL _cmd) {
void(*block)(void) = ((void (*)())&__Test__test_block_impl_0((void *)__Test__test_block_func_0, &__Test__test_block_desc_0_DATA, self, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
发现底层函数默认添加了两个参数:self
和 _cmd
,这也是我们为什么能在函数内可以调用到 self 和 _cmd 的原因。
若假如打印 _param 呢?运行
Test* t = [[Test alloc] initWithParam:@"something"];[t test];
发现可打印:
something
此时是捕获的 _param?错,_param 等价于 self->_param
,所以捕获的还是 self。
block 的类型
block 有三种类型,亦是可以通过 class
方法或者查看 isa 指针查看其具体类型,但最终都是继承自 NSBlock
。
类型 | |
---|---|
_NSGlobalBlock_ | 全局 block |
_NSStackBlock_ | 栈区 block |
_NSMallocBlock_ | 堆区 block |
我们为探究其类型,运行:
void(^block)(void) = ^{
NSLog(@"This is a block");
};
block();
NSLog(@"%@", [block class]);
打印:
This is a block
__NSGlobalBlock__
追加打印:
NSLog(@"%@", [[block class] superclass]);
NSLog(@"%@", [[[block class] superclass] superclass]);
NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
得:
__NSGlobalBlock
NSBlock
NSObject
现在得到继承链:
那么现在打印:
void(^block)(void) = ^{
NSLog(@"This is a block");
};
int num = 10;
void(^block1)(void) = ^{
NSLog(@"The num is %d", num);
};
NSLog(@"%@ %@ %@", [block class], [block1 class], [^{
NSLog(@"The num is %d", num);
} class]);
得:
__NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__
重写后我们可看见有三个 block:__main_block_impl_0、__main_block_impl_1、__main_block_impl_2。
但奇怪的是三者得 isa 指针都指向的是 _NSConcreteStackBlock
。
为什么会出现这样的问题,是因为在重写命令中通过 clang
转成的 C++ 代码并不能完全代表 Objective-C 最终的底层实现。
所以我们还是按照打印的标准也判断 block 的类型,可发现, block 的存储类型和捕获外部的局部变量也有关系。
text 区存放的是程序代码,data 区存放的是全局变量,堆区放的是 alloc 出来的对象,动态分配内存,需要开发者手动调用,也需要开发者主动管理内存(现在有 ARC 了),栈区放的是局部变量,系统自动销毁内存。
具体的 block 类型是区分的?如下表:
block 类型 | 区别 |
---|---|
_NSGlobalBlock_ | 没有访问 auto 变量 |
_NSStackBlock_ | 访问 auto 变量 |
_NSMallocBlock_ | _NSStackBlock_ 调用了 copy |
对于 NSStackBlock 的 block 存在一个问题,代码如下:
void(^block)(void);
void test() {
int num = 35;
block = ^{
NSLog(@"The num is %d", num);
};
NSLog(@"%@", [block class]);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block();
}
}
运行结果为:
__NSStackBlock__
The num is -272632472
做这个实验请将内存管理改为手动(MRC):Build Setting -> Objective-C Automatic Reference Counting -> No
为何会出现 -272632472?是因为,执行过 test()
后栈区的 对应数据被回收,存在的可能就是垃圾数据,那么再访问结构体内的成员的时候得到的就是这些垃圾数字。
将上述代码稍作改动,test 内的 block 改为:
block = [^{
NSLog(@"The num is %d", num);
} copy];
打印结果为:
__NSMallocBlock__
The num is 35
此时的 block 已经进行了 copy 操作,栈 block 变为堆 block,内存需要我们手动释放,而我并没有释放,所以打印的 num 是正确的。
产生疑惑,_NSGlobalBlock_ 类型的栈进行了 copy 操作会变成 _NSMallocBlock_ 类型吗?
去掉 block 内部对 num 的打印再来运行发现:
__NSGlobalBlock__
即使使用了 copy 操作,block 依然为 _NSGlobalBlock_ 类型。
copy 操作
由上节可知,对于 _NSStackBlock_ 类型的 block 有太多的不确定性,所以在对这种 block 使用的时候需要对其进行一次 copy
操作将栈 block 复制到堆区。
但上节的例子是基于 MRC 的环境下操作的,在 ARC 的环境下,编译器会根据情况自动讲 block 进行 copy 操作。
在 ARC 环境下执行:
void(^block)(void);
void test() {
int num = 35;
block = ^{
NSLog(@"The num is %d", num);
};
NSLog(@"%@", [block class]);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block();
}
}
得:
__NSMallocBlock__
The num is 35
在以下条件下,编译器会自动将 block 进行 copy 操作:
- block 作为返回值
- 将 block 复制给 __strong 指针时
-
block 作为 Cocoa API 中方法名含有 usingBlock 的方法参数时
如:
NSArray* arr = ...;
[arr enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
对象类型的 auto 变量捕获
前面的例子内部捕获外部变量都是基本类型,如 int,那么对象类型的外部变量是如何捕获的?
将 Test 类,添加 NSInteger 类型的属性 num。
外部:
Test* test = [[Test alloc] init];
test.num = 35;
TestBlock block = ^{
NSLog(@"The num is %ld", (long)test.num);
};
block();
TestBlock 定义为:typedef void(^TestBlock) (void);
运行得:
The num is 35
我们稍作改动:
TestBlock block;
{
Test* test = [[Test alloc] init];
test.num = 35;
block = ^{
NSLog(@"The num is %ld", (long)test.num);
};
}
NSLog(@"=====end=====");
也重写了 Test 的 dealloc()
方法打印 dealloc。我们增加断点在打印 “end” 的一行,运行发现断点处,并没有打印 Test 的 delloc 信息,也就是说,内部 {}
执行完了 Test 也没有立即被销毁。
我们将代码改成:
Test* test = [[Test alloc] init];
test.num = 35;
TestBlock block = ^{
NSLog(@"The num is %ld", (long)test.num);
};
NSLog(@"=====end=====");
重写后发现 block 的结构体中有 Test *test
成员变量。回到修改之前的代码,在执行:
block = ^{
NSLog(@"The num is %ld", (long)test.num);
};
的时候,block 进行了 copy 操作成为堆区的 block,不会轻易销毁,那么意味着对 test 也是强引用持有,test 亦不会轻易被释放,所以 dealloc 信息延后打印:
=====end=====
=====dealloc=====
若是 MRC 环境(需添加 [t release]
操作,并且 dealloc 方法内须调用父类的 dealloc 方法),即使 block 还在,也会先执行 Test 的 dealloc 方法。结果为:
=====dealloc=====
=====end=====
若在 MRC 环境下改为:
block = [^{
NSLog(@"The num is %ld", (long)test.num);
} copy];
则会达到 ARC 下同样的效果,因为进行了 copy 操作后在 block 内部相当于调用了一次 [t reatain]
操作。结果为:
=====end=====
=====dealloc=====
回到 ARC 环境,假如 Test 对象进行 __weak 修饰,则情况又有所不同:
=====dealloc=====
=====end=====
在用 __weak 修饰的情况下重写 C++ 代码会报错:
cannot create __weak reference because the current deployment target does not support weak references
是因为命令需要支持 ARC 并且指定运行时系统版本,如:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-9.0.0 main.m
重写成功后发现 block 结构体为:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Test *__weak test;
...
};
test 对象为 weak
修饰,所以在离开作用域后立即释放。去掉 weak 后的结构体再用上命令重写,得到:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Test *__strong test;
...
};
发现 weak 默认用了 strong
修饰,所以“延长了”其寿命。
最后来个总结:
- 当 block 在栈上,不会对 auto 变量产生强引用
- 当 block 在堆上,会根据 auto 是否由 __strong 或者 —__weak 修饰来决定是否产生强引用 [下有说明]
- 当 block 从堆上移除,将放弃对 auto 变量的引用,相当于进行了一次
release
操作
copy 操作后的 block 其 Desc
是有变化的:
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};
原本只有 reserved
和 Block_size
现在又多了两个函数指针: copy
和 dispose
。copy 保存的是 __main_block_copy_0
,dispose 保存的是 __main_block_dispose_0
。
当 block 执行了 copy 操作后,这两个函数便会执行。
__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) {
// 会根据 test 对象是 strong 还是 weak 修饰来决定是否对 test 对象产生强引用
_Block_object_assign((void*)&dst->test, (void*)src->test, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
// 对 test 对象进行释放
_Block_object_dispose((void*)src->test, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
函数 | 调用时机 |
---|---|
copy | 栈上的 block 复制到堆时 |
dispose | 堆上的 block 被收回时 |
__block
我们再来新建一个例子工程:
typedef void(^TestBlock) (void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int num = 10;
TestBlock block = ^{
NSLog(@"The num is %d", num);
};
block();
}
return 0;
}
运行上面这段代码,结果为:
The num is 10
那么实际情况中,我们常常需要在 block 内部改变外面变量的值,在 block 内部直接修改是不允许的:
^{
num = 35; // ✘
}
这是因为 num 的作用域属于 main 函数,而 block 内执行逻辑属于另一个函数 __main_block_func_0
,是无法跨域进行修改的。
但是通过 static
修饰的局部变量是可以用这种方式修改的:
static int num = 10;
TestBlock block = ^{
num = 35;
NSLog(@"The num is %d", num);
};
block();
结果为:
The num is 35
因为 static 修饰的是引用传递,block 的结构体存储的是指向 num 的指针,所以在内部修改 num 的值是可以成功的。
那么如何修改非 static 修饰的的局部变量?就是 __block
关键字。
__block int num = 10;
TestBlock block = ^{
num = 35;
NSLog(@"The num is %d", num);
};
block();
结果:
The num is 35
__block 本质
__block 变量不能修饰全局变量、静态变量。并且编译器会将 __block 变量包装成一个对象。
重写 C++ 代码后发现 block 结构体 num 的成员变量和之前未用 __block 修饰的 num 有本质的区别:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_num_0 *num; // by ref
...
};
这里的 num 为 __Block_byref_num_0 *
类型。__Block_byref_num_0 也是个结构体,其内部定义是这样的:
struct __Block_byref_num_0 {
void *__isa;
__Block_byref_num_0 *__forwarding;
int __flags;
int __size;
int num;
};
我们可推断一开始 num 的值为 10,这个值一定是存储在 __Block_byref_num_0 的成员变量 num 中。那么 __forwarding
表示什么?
首先我们看到由 __block 修饰后的 num,在 main 函数的源码中变成了:
__attribute__((__blocks__(byref))) __Block_byref_num_0 num = {(void*)0,(__Block_byref_num_0 *)&num, 0, sizeof(__Block_byref_num_0), 10};
简化版本:
__Block_byref_num_0 num = {(0,
&num,
0,
sizeof(__Block_byref_num_0),
10};
此时第一个 0 赋值给 __isa,第二个 0 赋值给 __flags,第四个参数是计算当前结构体有多大并赋值给 __size,最后 10 赋值给 num,推断得到验证。第二个参数 &num
就是 num 结构体本身,也就是说它将自身的结构体地址传递给了 __forwarding。换而言之 __forwarding 指向的是自己。
同时 &num 也传给了 __main_block_impl_0 的 *num
:
TestBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_num_0 *)&num, 570425344));
block 修改 num 的源码为:
// 首先拿到 __Block_byref_num_0 中的 __forwarding
__Block_byref_num_0 *num = __cself->num;
// 取得 num 再修改
(num->__forwarding->num) = 35;
倘若多加了一个对象类型的局部变量:
__block int num = 10;
__block NSObject* obj = [[NSObject alloc] init];
TestBlock block = ^{
obj = nil;
num = 35;
NSLog(@"The num is %d", num);
};
block();
num 和 obj 在底层会生成两个机构体:
struct __Block_byref_num_0 {
void *__isa;
__Block_byref_num_0 *__forwarding;
int __flags;
int __size;
int num;
};
struct __Block_byref_obj_1 {
void *__isa;
__Block_byref_obj_1 *__forwarding;
int __flags;
int __size;
// copy 操作
void (*__Block_byref_id_object_copy)(void*, void*);
// dispose 操作
void (*__Block_byref_id_object_dispose)(void*);
NSObject *__strong obj;
};
block 结构体会有两个成员变量指向它们在这里不贴出。
我们去掉对象类型的 obj 回到最简状态,在 block()
后打印 num 的内存地址,得:
0x10051e968
这个内存地址和底层的谁有对应关系?是 __main_block_impl_0 中的 *num?还是 __Block_byref_num_0 中的 num?我们自己实现这些低层结构:
然后运行:
__block int num = 10;
TestBlock block = ^{
num = 35;
NSLog(@"The num is %d", num);
};
struct __main_block_impl_0* blockStruct = (__bridge struct __main_block_impl_0*)block;
NSLog(@"%p", &num);
在最后一行加断点发现 __Block_byref_num_0 * 型 num 的地址为:0x000000010204b490,打印局部变量的 num 为 0x10204b4a8,两者并不相同。
0x000000010204b490 为 __Block_byref_num_0 * 型 num 的地址也就意味着是 __isa 的地址,那么 age 的地址是什么?
__isa 大小为 8,__forwarding 大小为 8(地址为 0x000000010204b498),__flags 大小为 4(地址为0x000000010204b4a0), __size 大小为 4(地址为0x000000010204b4a4),num 的地址为 0x000000010204b4a8。是不是很眼熟?没错 num 的地址和外部变量的 num 一样。
通过:
print/x &(blockStruct->num->num)
命令得到的打印结果和 NSLog(@"%p", &num);
得到的结果也是一样的也可以验证。
__block 内存管理
我们来看这个熟悉的例子:
int num = 0;
TestBlock block = ^{
NSLog(@"%d", num);
};
block();
底层的 __main_block_desc_0
是没有 copy
和 dispose
两个成员函数的,但是当 num 用 __block 的时候就多了这两个函数,并在 copy 函数中调用 _Block_object_assign()
对 结构体中的 __Block_byref_num_0 *num
进行内存管理。
假如有 Block 0 和 Block 1 分别对 __block 变量引用,则:
在 ARC 环境下首先 Block 0 会 copy 到堆上,然后 __block 修饰的变量也同样会 copy 到堆上,然后进行强引用。
然后 Block 1 也会 copy 到堆上并对 __block 变量有强引用:
当 block 从堆上移除的时候,首先会调用内部 dispose 函数,其内部会调用 _Block_object_dispose()
函数,然后释放 __block 变量:
若外部是:
__block int num = 0;
__block NSObject* obj == ...;
TestBlock block = ^{
...
};
block();
则底层对 int 和 obj 都会产生强引用。
_Block_byref名字_0 就是强引用
若:
__block int num = 0;
NSObject* obj = [[NSObject alloc] init];
__weak NSObject* weakObj = obj;
TestBlock block = ^{
...
};
block();
则底层不会对 weakObj 产生强引用。
另,我们在 C++ 代码中看到:
__main_block_impl_0*src) {
_Block_object_assign((void*)&dst->num, (void*)src->num, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_assign((void*)&dst->weakObj, (void*)src->weakObj, 3/*BLOCK_FIELD_IS_OBJECT*/);}
8 表示 __block 修饰的变量,对应注释:BLOCK_FIELD_IS_BYREF
3 表示对象,对应注释:BLOCK_FIELD_IS_OBJECT
__block 的 __forwarding 指针
当 block 在栈上时,__forwarding 指针指向自己。那么堆上的 __forwarding 指向谁呢?答案也是自己,但是需要注意的是,经过 copy 操作后,原栈上的 __forwarding 指针指向堆上的 block,即:
循环引用问题
当对象对 block 本身有强引用,而 block 又对对象持有,则会引发循环引用。如:
Test* t = [[Test alloc] init];
t.num = 35;
t.block = ^{
NSLog(@"%ld", t.num);
};
ARC
使用 __weak 和 __unsafe_unretained 解决
在 ARC 环境下可通过,__weak
和 __unsafe_unretained
解决:
Test* t = [[Test alloc] init];
t.num = 35;
__weak Test* weakT = t;
t.block = ^{
NSLog(@"%ld", weakT.num);
};
或者:
Test* t = [[Test alloc] init];
t.num = 35;
__weak typeof(t) weakT = t;
t.block = ^{
NSLog(@"%ld", weakT.num);
};
对于 self 的情况也是同理:
__weak typeof(self) weakSelf = self;
__unsafe_unretained 同理,但 __unsafe_unretained 是不安全的,若 __weak 指向的对象销毁,则 weakXXX 会自动置为 nil
,但 __unsafe_unretained 不会,它还是会指向那个销毁对象的地址,所以进行访问 weakXXX 的时候很有可能产生野指针错误。
使用 __block 解决
__block 情况下的循环应用如下:
在必须调用 block 的情况下还可以使用 __block 来解决。
__block id weakSelf = self;
并且 block 内部的 weakSelf 要职位 nil:
xxx.block = ^{
...
weakSelf = nil;
};
因为一旦 weakSelf 置为 nil,三者互相“僵持不下”的状态就会打破,也就不存在循环引用的问题了。
MRC
使用 __unsafe_unretained 解决
同 ARC 环境的方式一样。
MRC 下不支持 __weak。
使用 __block 解决
在 MRC 环境下使用 __block 修饰的话在底层是不会对外部变量进行 retain 也就是强引用操作的,而 ARC 会。
并且不需要调用 weakSelf = nil
就可以解决循环引用的问题。