[toc]
参考
http://www.cocoachina.com/ios/20150106/10850.html
https://www.jianshu.com/p/404ff9d3cd42
OC代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block NSInteger val = 0;
// 访问 val, 实际访问的是 val.__forwarding->val , 此时在栈上
NSLog(@"1_val = %ld - %p", val, &val);
void (^block)(void) = ^{
// 这里捕获 val, 捕获的是被 __block 包装的 val 对象;
// 与否被重新赋值没有关系;
val = 1;
NSLog(@"2_val = %ld - %p", val, &val);
};
NSLog(@"3_val = %ld - %p", val, &val);
block();
NSLog(@"4_val = %ld - %p", val, &val);
}
return 0;
}
MRC 输出: (变量值 - 变量地址)
// __block变量始终在栈上, 而且是同一份地址
// 因为block捕获的是val的地址, block没有被拷贝到堆, 那val也还是最初的val ★
1_val = 0 - 0x7ffeefbff408
3_val = 0 - 0x7ffeefbff408
2_val = 1 - 0x7ffeefbff408
4_val = 1 - 0x7ffeefbff408
ARC 输出:
1_val = 0 - 0x7ffeefbff408 // block定义前: 栈地址
3_val = 0 - 0x103308438 // block定义后: 堆地址 ★
2_val = 1 - 0x103308438
4_val = 1 - 0x103308438
// block 访问 __block 修饰的局部变量, block定义前后, 局部变量指针的地址不一致, 且地址值相差较大, 说明变量 val 已拷贝到堆中, 且 block 外局部变量 val 的地址也被改为这个堆地址。
分析:
MRC下:
block 默认不被 copy, block 始终在栈上; <block - 存储域>
所以无论包内包外, 该对象都不被copy, 始终在栈上; 局部变量 (其结构体成员) 的地址也始终在栈上, 且始终是同一份地址;
ARC下:
block 因被强指针引用, 在定义之后, 不管有没有被调用, block 就已经被拷贝到堆上了; <block - 存储域>
__block
将局部变量 var 包装成了对象(结构体)__Block_byref_val_0
, 并生成其 var, 我们访问的 var, 实际是通过结构体实例 var 的成员指针__forwarding
间接访问成员变量 var。-
变量 val 所包装成的的结构体
__Block_byref_val_0
实例 (对象的指针) :在 block 定义前, 在栈区。
-
在 block 定义后, 结构体 val 的地址 被 block 捕获为成员变量, 随 block 拷贝到堆 ;
此时, 包外的结构体 val 的 __forwarding 指向堆中val的新地址。<详见本文C++分析>
-
后续对 val 的读写也都是在这个堆地址上进行, 无论是否触发block;
故而block包内可以修改局部变量 val 的值。
可见:
__block
实现了变量堆栈地址的变更 , 而有些博客非所谓的 "写操作生效"。block 访问
__block
修饰的局部变量, 会将该变量同 block 一起 copy 到堆区;
C++代码
MRC / ARC 编译后代码一致:
int main(int argc, const char * argv[]) {
{ __AtAutoreleasePool __autoreleasepool;
// 定义 __block局部变量 __block NSInteger val = 0;
// __block局部变量 val 被封装成了一个 __Block_byref_val_0 结构体类型的实例 val
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 0};
// 访问 val 实际都是 val.__forwarding->val ★★
NSLog((NSString *)&__NSConstantStringImpl__var_folders_p5_mp3284bs2xb073r91w__n99r0000gn_T_main_060736_mi_0, (val.__forwarding->val), &(val.__forwarding->val));
// 捕获的是结构体 val 的地址, 作为第3个入参
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_p5_mp3284bs2xb073r91w__n99r0000gn_T_main_060736_mi_2, (val.__forwarding->val), &(val.__forwarding->val));
// block调用
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_p5_mp3284bs2xb073r91w__n99r0000gn_T_main_060736_mi_3, (val.__forwarding->val), &(val.__forwarding->val));
}
return 0;
}
// 捕获到的局部变量, 被封装成结构体类型 (包装成对象);
// 其名称 __Block_byref_val_0 是根据捕获到的局部变量名 val 命名的
// 这个结构体中包含了该实例本身的引用 __forwarding ★★
// 访问变量 val, 实质访问的是结构体 __Block_byref_val_0 的成员变量 val (val.__forwarding->val)
struct __Block_byref_val_0 {
void *__isa; // 有isa, 对象的特征; 编译器将 __block 变量包装成了对象 ★
__Block_byref_val_0 *__forwarding; // 该实例本身的引用 ★
int __flags;
int __size;
NSInteger val; // 结构体内部保存的原始变量 ★
};
// block 本身被转换成了 __main_block_impl_0 结构体实例;
// 该实例持有 __Block_byref_val_0 结构体实例的指针。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 包装了局部变量的对象(结构体指针) ★★
__Block_byref_val_0 *val; // by ref
// 构造函数, 注意第3个入参
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
impl.isa = &_NSConcreteStackBlock; // 栈中的 block, 出栈时会被销毁 (见下面<分析>) ★
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
// 通过入参 __cself 找到其成员变量, 结构体 __Block_byref_val_0 的实例 val ;
__Block_byref_val_0 *val = __cself->val; // bound by ref;
// 通过 __forwarding 找到<活跃>的结构体val, 拿到初始的局部变量val; ★
(val->__forwarding->val) = 1;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_p5_mp3284bs2xb073r91w__n99r0000gn_T_main_060736_mi_1, (val->__forwarding->val), &(val->__forwarding->val));
}
// 注意, 使用了 __block 修饰基本数据类型的局部变量, desc结构体中多了 copy 和 dispoose 函数 ★
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 };
// (目的destination 源source)
// 当 Block 从栈复制到堆时, 会调用 _Block_object_assign 函数持有该变量(相当于retain)。
static void __main_block_copy_0 (struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}
// 当堆上的 Block 被废弃时, 会调用 _Block_object_dispose 函数释放该变量(相当于release)。
static void __main_block_dispose_0 (struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}
分析
首先提几点疑问:
- 当 block 作为回调执行时, 局部变量 val 已经出栈了, 此时代码为什么还能work呢?
为什么是通过成员变量
__forwarding
, 而不是直接去访问结构体中需要修改的变量呢?-
从上述C++代码中可以看到,
__main_block_impl_0
结构体构造函数中, isa指向的是_NSConcreteStackBlock
, 即在栈上生成。分配在全局的 block, 在作用域外也可以通过指针安全的访问。但栈上的 block, 如果它所属的作用域结束, 该Block就被废弃。
同样,
__block
变量在定义时, 也分配在栈上, 当超出该变量的作用域时, 该__block
变量也会被废弃。咋办呢?
该 _NSConcreteMallocBlock
登场了!
苹果采用了将 Block 和 __block
变量从栈上复制到堆上, 来解决这个问题。
复制到堆上的Block, 它的结构体成员变量 isa 将变为: impl.isa = &_NSConcreteMallocBlock
;
当 Block 被复制到堆上时, 其捕获的 __block
变量也会被复制到堆上, 此时堆上的 Block 持有相应的堆上的 __block
变量。
当栈上的 Block 及捕获的变量超出它原本作用域时, 堆上的 Block 还可以继续存在。
当堆上的 __block
变量没有持有者时, 它才会被销毁。(这里的思考方式和 objc 引用计数内存管理完全相同。)
此时, 只要原先栈上的 __block
变量的成员变量 __forwarding
指向堆上的结构体实例, 就能够安全地访问。
一般可以使用
copy
方法手动将 Block 或者__block
变量从栈复制到堆上。
比如我们把 Block 做为类的属性访问时, 一般把该属性设为 copy。
有些情况下我们可以不用手动复制, 参考<存储域 - 自动 copy到堆>
__forwarding
指针图解
栈上的 __block
变量访问自身, 如图:
<img src="https://cdn.jsdelivr.net/gh/coder-felix/image/20200609225314.png" style="zoom:25%;" />
__block
变量被复制到堆, 此时栈上和堆上分别有一个 __block
变量 (结构体)
<img src="https://cdn.jsdelivr.net/gh/coder-felix/image/20200609225355.png" style="zoom:25%;" />
首先明确一点: block 和 __block
变量, 实质就是相应结构体的实例。
__block
变量在栈上时,__forwarding
指针就指向自己。-
block 拷贝到堆上后,
__block
变量随 block 也拷贝到堆上一份, 此时有两个__block
变量:堆上的这个
__block
变量的__forwarding
指针指向自己;原先栈上的
__block
变量的__forwarding
指针改为指向堆上的__block
变量。
这个时候我们可以通过 val.__forwarding->val
访问变量。
val.__forwarding->val
拿到一个结构体 val
, 然后通过它的 __forwarding
找到<活跃>的结构体 val
, 从而访问结构体内部保存的原始局部变量 val
。
如果此时结构体 val 还未被拷贝到堆, 那<活跃>的结构体 val 就是最开始定义的, 在栈上;
如果此时结构体 val 已经被拷贝到堆, 那<活跃>的结构体 val 就是这个被拷贝到堆上的结构体;
这保证了无论结构体 val 有没有被拷贝到堆, 无论是从堆上还是栈上访问, 访问到的原始局部变量始终是同一个。★
注: <活跃> 是个人为方便理解而定义的:
- 如果未被拷贝到堆, 那<活跃>就是指栈上的;
- 如果已被拷贝到堆, 那<活跃>就是指堆上的;