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大分区和作用,简要记录一下:

  1. 栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。

  2. 堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由系统回收 。ARC下虽然不用我们管,其实是由于编译器已经帮我们在合适的位置插入了retain/release/autorelease等而已。

  3. 全局区(静态区)(static)—全局变量和静态变量的存储是放在一块的。初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。

  4. 文字常量区 —常量字符串/Const常量就是放在这里的。 程序结束后由系统释放。

  5. 程序代码区—存放函数体的二进制代码。

详细的可以看深入浅出-iOS内存分配与分区

以上是关于Block中是如何实现截获自动变量值的呢?的主要内容,如果未能解决你的问题,请参考以下文章

block理解

Kotlin函数 ⑤ ( 匿名函数变量类型推断 | 匿名函数参数类型自动推断 | 匿名函数又称为 Lambda 表达式 )

如何根据变量值“n”自动填充单元格

Clang 是如何编译Block的?

如何在调试 MSVC ABI Rust 程序时检查变量值?

delphi如何做到一个自动运行的控件,如下图定时器