Block中是如何实现截获自动变量值的呢?
Posted Haley_Wong
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Block中是如何实现截获自动变量值的呢?相关的知识,希望对你有一定的参考价值。
我们都说block会捕获(持有)它使用到的局部变量的值,可是它是如何实现捕获自动变量的值的呢?
下面依然是使用一段代码,然后用Clang进行转换,来分析其过程。
1.使用Clang对比转换前后的代码
转换前的main.m源码:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[])
@autoreleasepool
int a = 10;
void (^blk)(void) = ^
printf("Block---a:%d \\n", a);
;
a = 20;
blk();
return 0;
转换后的main.cpp中block相关代码:
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---a:%d \\n", 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 argc, const char * argv[])
/* @autoreleasepool */ __AtAutoreleasePool __autoreleasepool;
int a = 10;
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
a = 20;
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
2 分析转换后的代码与之前没有截获自动变量的block的区别
可以看到转换后__main_block_impl_0
结构体比上一篇中的结构体多了一个int a
实例变量,并且__main_block_impl_0
的构造函数也多了一个参数,构造函数的初始化部分也发生了一些变化。
由此推断,在__main_block_impl_0
中会多出其使用到的变量存储位置,然后在其构造函数中,将使用到的局部变量值存储到结构体实例中。
__main_block_func_0
的实现过程,也发生了一些变化。
在使用局部变量时,是先将其从__main_block_impl_0
实例的实例变量中取出,然后再使用这个自己存储的值,所以不管外部如何修改,都不影响内部的这个值。
简单总结下来,就是block中使用到的局部变量,都会在编译时动态创建的block实现结构体中创建一个与局部变量名称一样的实例变量,该实例变量存储着外部的局部变量的值,而当执行block时,再将这里存储下来的值取出来,所以这外部和block内部使用的是两个不同的变量,因此即使外部先修改了外部变量的值,再执行block,也不会影响到block内的实例变量的值。
3. 验证block中截获多个自动变量的情况
转换前main.m中的测试代码:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[])
@autoreleasepool
int a = 10;
int b = 20;
void (^blk)(void) = ^
printf("Block---a:%d,---b:%d \\n", a, b);
;
a = 20;
blk();
return 0;
转换后的main.cpp中的block代码:
struct __main_block_impl_0
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
int b;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int _b, int flags=0) : a(_a), b(_b)
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
int b = __cself->b; // bound by copy
printf("Block---a:%d,---b:%d \\n", a, b);
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;
int a = 10;
int b = 20;
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, b));
a = 20;
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
从转换后的代码,可以看出与推断的一致。
4.验证block中截获对象类型局部变量
原始代码:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[])
@autoreleasepool
int a = 10;
NSString *name = @"zhangsan";
void (^blk)(void) = ^
// printf("Block---a:%d,---name:%@ \\n", a, name);
NSLog(@"Block---a:%d,---name:%@", a, name);
;
name = @"lisi";
a = 20;
blk();
return 0;
转换后的代码:
struct __main_block_impl_0
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
NSString *name;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, NSString *_name, int flags=0) : a(_a), name(_name)
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
NSString *name = __cself->name; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_rp_rp_9nrd50kq_0n87d_r7fn1c0000gn_T_main_f4c9c1_mi_1, a, name);
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, 3/*BLOCK_FIELD_IS_OBJECT*/);
static void __main_block_dispose_0(struct __main_block_impl_0*src) _Block_object_dispose((void*)src->name, 3/*BLOCK_FIELD_IS_OBJECT*/);
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;
int a = 10;
NSString *name = (NSString *)&__NSConstantStringImpl__var_folders_rp_rp_9nrd50kq_0n87d_r7fn1c0000gn_T_main_f4c9c1_mi_0;
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, name, 570425344));
name = (NSString *)&__NSConstantStringImpl__var_folders_rp_rp_9nrd50kq_0n87d_r7fn1c0000gn_T_main_f4c9c1_mi_2;
a = 20;
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
从转换后的源码,可以看出block中使用的对象类型,依然会在__main_block_impl_0
中添加一个同样类型的实例变量。然后依然是创建__main_block_impl_0
变量时,将对象对应的指针赋值给内部实例变量。后面变量时,也是用内部实例变量。
但是捕获对象类型时,__main_block_desc_0
的结构体会有一些变化,多了两个函数。这两个函数分别是用来拷贝变量和释放变量的。这个再后面篇章再介绍。
5.为何无法在block中修改外部的局部变量的值?
通过上面的转换后的代码,我们已经知道了,在block执行函数中,使用的是__main_block_impl_0
中存储的实例变量。所以,如果我们修改这个实例变量的值,修改的也是__main_block_impl_0
中存储的实例变量的值,无法修改到外部的局部变量,而实际上,我们在block中是想修改外部的局部变量的值,所以针对这种情况编译器就给我们报错,来提醒我们:
Variable is not assignable (missing __block type specifier)
注意:这里指的是修改没有__block修饰符的局部变量。
那有没有特殊情况呢?
答案是有的,有三种特殊的变量即使不使用__block,也可以在block中修改的:
- 静态全局变量
- 全局变量
- 静态变量
我们依然使用Clang来转换一下试试看。
测试用的原始代码:
#import <Foundation/Foundation.h>
static int static_global_value = 1;
int global_value = 2;
int main(int argc, const char * argv[])
@autoreleasepool
static int static_value = 3;
void (^blk)(void) = ^
static_global_value = 2;
global_value = 4;
static_value = 6;
;
blk();
NSLog(@"static_global_value:%d---global_value:%d----static_value:%d",static_value);
return 0;
然后使用Clang命令(clang -rewrite-objc main.m
)转换。
转换后的block代码如下:
static int static_global_value = 1;
int global_value = 2;
struct __main_block_impl_0
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_value;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_value, int flags=0) : static_value(_static_value)
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
;
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
int *static_value = __cself->static_value; // bound by copy
static_global_value = 2;
global_value = 4;
(*static_value) = 6;
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;
static int static_value = 3;
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_value));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_rp_rp_9nrd50kq_0n87d_r7fn1c0000gn_T_main_f370cb_mi_0,static_value);
return 0;
从以上代码可以看出,block并不会捕获全局变量,而函数内的静态变量,虽然Block的结构体内部依然也有一个变量,但是该变量存储的确是静态变量的指针,在执行block时,因为使用的是内部存储的外部变量的指针,所以使用指针修改后,就等价于外部的静态变量也被修改了。
那既然可以存储变量的指针,block中为何存储局部变量的值,而不是存储指针呢?
这跟内存区域有关!
我们知道内存区域有:全局区、堆区、栈区、常量区、程序代码区。
因为静态变量和静态全局变量是存储在全局区,它们是在程序结束后,由系统释放。所以出了函数作用域外,这些变量依然存在,那么block中就依然可以正常使用。
但是局部变量,虽然可以使用变量指针存储,但是出了函数作用域,这些变量就被释放了,那么我们就不能在block中正常访问了。
因此block中是创建一个新的实例变量来存储其捕获的局部变量,而不是创建一个指针来存储捕获的局部变量。
关于内存的5大分区和作用,简要记录一下:
-
栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。
-
堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由系统回收 。ARC下虽然不用我们管,其实是由于编译器已经帮我们在合适的位置插入了retain/release/autorelease等而已。
-
全局区(静态区)(static)—全局变量和静态变量的存储是放在一块的。初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。
-
文字常量区 —常量字符串/Const常量就是放在这里的。 程序结束后由系统释放。
-
程序代码区—存放函数体的二进制代码。
详细的可以看深入浅出-iOS内存分配与分区
以上是关于Block中是如何实现截获自动变量值的呢?的主要内容,如果未能解决你的问题,请参考以下文章
Kotlin函数 ⑤ ( 匿名函数变量类型推断 | 匿名函数参数类型自动推断 | 匿名函数又称为 Lambda 表达式 )