利用Clang探究block捕获外部变量的本质(一)

前言

说到外部变量,C语言中变量一般可以分为5种:

  • 自动变量
  • 函数参数
  • 静态变量(指局部静态变量)
  • 静态全局变量
  • 全局变量

我们知道,Objective-C的block会捕获自动变量。在计算机编程领域,自动变量(Automatic Variable)指的是局部作用域变量,即局部变量。相对于全局变量。如下,在main函数中声明一个局部变量val = 1;block中打印val的值,然后在执行block前修改val = 2;, 但是block依旧输出1。这就是所谓的block会捕获自动变量。
本篇文章主要探究block捕获局部变量的底层原理。除去函数参数外,关于block和静态变量、全局变量、静态全局变量的关系将在后面的文章展开讨论。

Objective-C转C++

int main() {
    // block会捕获局部变量,下面执行执行block前后val分别输出2和1
    int val = 1;
    void (^block)(void) = ^{
        printf("执行block后val = %d, val地址 = %p\n", val, &val);
    };
    val = 2;
    printf("执行block前val = %d, val地址 = %p\n", val, &val);
    block();

    return 0;
    
<!--    控制台打印:-->
<!--    执行block前val = 2, val地址 = 0x7ffeefbff598-->
<!--    执行block后val = 1, val地址 = 0x100713770-->
<!--    Program ended with exit code: 0-->
}

这里有两个疑问:

  • block是如何实现捕获block外部局部变量的?
  • 为什么block执行前后val的内存地址不同?

以上源码经过Clang转为C++代码,如下:

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int val;
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : val(_val) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int val = __cself->val; // bound by copy
    printf("执行block后val = %d, val地址 = %p\n", val, &val);
}

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 val = 1;
    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val));
    val = 2;
    printf("执行block前val = %d, val地址 = %p\n", val, &val);
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

C++源码分析

如果你读过笔者的利用Clang探究block的本质利用Clang探究__block的本质,那么本篇文章理解起来将会是切菜一样easy。甚至本篇文章变得有些多余。
以上代码看起来很熟悉,和利用Clang探究block的本质一样,依旧包括3个结构体和1个函数。唯一不同的是结构体__main_block_impl_0中多了一个成员变量int val;而函数__main_block_func_0中使用__cself->val又初始化了另一个局部变量val。
构造函数实现:

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
}

block函数指针实现:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int val = __cself->val; // bound by copy
    printf("执行block后val = %d, val地址 = %p\n", val, &val);
}

以上两段代码体现了block捕获局部变量的本质:编译器会根据上下文动态生成block实现的结构体__main_block_impl_0(之前文章有讲)。也会动态的在结构体中分配一个和局部变量同名同类型的变量val。在main函数中调用__main_block_impl_0构造函数时将局部变量val作为参数传递给__main_block_impl_0。__main_block_impl_0会把参数val保存在自己的成员变量val中。当执行block的函数实现__main_block_func_0时会把__main_block_impl_0实例作为入参传递给__main_block_func_0,__main_block_func_0将__main_block_impl_0的成员变量val保存的值取出后赋值给另一个临时变量val。这样就实现了block捕获外部变量。因为block在函数内部又创建了另一个临时变量val,这也验证了,为什么在block外和block内打印的val的内存地址不同。

也许你会疑问,为什么把val变量封装到了结构体__main_block_impl_0中,而不是结构体__block_impl中?
原因__block_impl这个结构体是“稳定的”、“不变的”。利用Clang探究block的本质 中说过,__block_impl这个结构体用来描述block的底层结构,包含4个成员变量。__block_impl是一个通用结构体,所谓通用是指其他block的底层结构依旧是__block_impl。如果一个Objective-C的文件中存在多个block,那么对应的C++文件依旧只存在一个__block_impl。而每个block的具体实现均对应一个类似于__main_block_impl_0的结构体,所以,把val封装到block对应的实现结构体中。

敬请期待~

推荐阅读更多精彩内容