Block捕获__block局部变量的底层原理
更新记录
时间 | 版本修改 |
---|---|
2020年5月8日 | 初稿 |
1. 前言
上篇文章《Block中修改局部变量的值为什么必须声明为__block类型》中,考虑到篇幅不宜过长,并没有给出探索Block捕获__block
局部变量的代码例子。本文准备较详细地探索Block捕获__block
局部变量的底层原理,也作为上篇文章的补充说明
2. Block捕获__block
局部变量代码剖析
2.1 Block捕获__block
局部变量代码示例
- Objective-C代码如下:
#include <stdio.h>
int main(int argc, char * argv[]) {
__block int val = 10;
const char *fmt = "val = %d\n";
void (^blk)(void) = ^{
++val;
printf(fmt,val);
};
val = 2;
fmt = "These value were changed. val = %d\n";
blk();
return 0;
}
- 代码输出结果为:
val = 3
,可见val = 2
的赋值是起作用的,但是fmt
的赋值是不影响block内部的。
2.2 使用clang转换后的C++源代码剖析
- 转换后的源代码如下:
struct __Block_byref_val_0 {
void *__isa; //isa指针,指向类对象;(可类比NSObject)
__Block_byref_val_0 *__forwarding; //指向 __Block_byref_val_0 指针;有时候会指向自己;后续文章会详细介绍此成员变量的作用
int __flags; //暂时可忽略
int __size; //记录本结构体(即__Block_byref_val_0)的内存带下
int val; //捕获的局部变量的存储字段
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
const char *fmt;
__Block_byref_val_0 *val; // by ref; (本来只是一个int变量,捕获__block变量,就变成这么一个结构体了)
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, __Block_byref_val_0 *_val, int flags=0) : fmt(_fmt), val(_val->__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_val_0 *val = __cself->val; // bound by ref
const char *fmt = __cself->fmt; // bound by copy
++(val->__forwarding->val);
printf(fmt,(val->__forwarding->val));
}
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*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 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, char * argv[]) {
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10}; //声明并初始化一个 __Block_byref_val_0 结构体,其中记录了捕获的成员变量的值 10
const char *fmt = "val = %d\n";
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, (__Block_byref_val_0 *)&val, 570425344)); //声明并初始化 __main_block_impl_0 结构体,其中保存了 val 变量的地址
(val.__forwarding->val) = 2; //注意,这里是通过__forwaring来访问的,才可以正确同步真正存储数值的变量上。(敲黑板)
fmt = "These value were changed. val = %d\n";
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
- 转换后的源代码和捕获非
__block
变量的源代码的区别- 局部变量使用
__Block_byref_val_0
结构体来表示 __Block_byref_val_0
结构体中含有isa
指针,和OC对象一致- 含有
__Block_byref_val_0 *__forwarding
成员变量,指向相同的结构体- 至于为什么需要
__forwarding
这个成员变量,后续会单独写一篇文章说明。
- 至于为什么需要
__main_block_desc_0
结构体中多了2个成员变量,都是函数指针,分别是__main_block_copy_0
函数和__main_block_dispose_0
函数- 这2个函数的作用,后续会单独写一篇文章说明。
- 局部变量使用
3. 总结
- 从转换之后的源代码可以看到:
- 添加了
__block
修饰符的局部变量,变成了一个结构体。结构体中保存了实际的变量数值。 - Block用结构体(即
__main_block_impl_0
)记录了包含实际存储变量(即int val
)的结构体```__Block_byref_val_0````的地址- 所以可以在Block中修改该变量并同步到真实的变量上。
- 所以在Block外部修改了局部变量(实质上是修改了
__Block_byref_val_0
结构体中的int val
),Block内部也会受到影响
- 添加了
- 为什么捕获
__block
局部变量不可以像《Block中修改局部变量的值为什么必须声明为__block类型》中提到的捕获局部静态变量的方式,直接捕获变量的地址?- 因为Block语法生成的Block上,可以存有超过其变量作用域的被截获对象(比如某个栈变量)。栈变量作用域结束时,该栈变量就被丢弃了。而如果Block仅仅只是记住了之前栈变量的内存地址,后续通过该地址访问到的值已经是一个随机值了。(也就是说,记录的指针已经变成了一个野指针。)