利用Clang探究block的本质

前言

block作为Objective-C语言中的一种特殊的存在,已经为大家所熟知。在其他语言中,也有类似于block的实现,比如JavaScript和Swift中的闭包,python中的lambda匿名函数。本篇文章主要讲解利用编译器前端clang来探究block的本质。关于clang的介绍请移步到LLVM简介Objective-C源文件编译过程

Objective-C转C++

我们可以借助clang的-rewrite-objc来把一个Objective-C的源文件转为C++文件。笔者示例一个main.m文件,文件源代码如下:

#import <Foundation/Foundation.h>

int main () {
    int a = 1, b = 2;
    int (^block)(int, int) = ^(int num1, int num2){
        return num1 + num2;
    };

    int sum = block(a,b);
    printf("%d\n", sum);
    return 0;
}

使用clang把main.m转化为C++源码。然后会生成一个C++文件。因为笔者的Objective-C源码中有#import <Foundation/Foundation.h>导致转化后的C++文件有3万多行。但关键代码就在最后30行,经过调整后(此处的调整是笔者对C++源码的位置进行调整,因为有些代码定义在文件的头部,有些代码在文件的尾部,导致阅读起来比较麻烦,笔者把文件首的代码粘贴到尾部)的关键源码如下:

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

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)};

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

static int __main_block_func_0(struct __main_block_impl_0 *__cself, int num1, int num2) {
    return num1 + num2;
}

int main () {
    int a = 1, b = 2;
    int (*block)(int, int) = ((int (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    int sum = ((int (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, a, b);
    printf("%d\n", sum);
    return 0;
}

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

转换后的C++代码主要包括3个结构体:__block_impl、__main_block_desc_0、__main_block_impl_0 和 一个函数 __main_block_func_0。我们从main函数切入,一步一步分析block的C++本质。

C++源码分析

上面已经说过,转换后的C++代码主要包括3个结构体和一个函数,下面我们逐个分析:

__block_impl

__block_impl是一个结构体,用来描述block的底层结构,包含4个成员变量。另外,__block_impl是一个通用结构体,所谓通用是指其他block的底层结构依旧是__block_impl。

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};
  • isa。和Objective-C对象一样,Block也包含一个isa指针,且isa指针作为结构体的第一个成员变量,指向block的所属类型。默认初始化为_NSConcreteStackBlock的地址。即impl.isa = &_NSConcreteStackBlock;
  • Flags。Flags作为结构体的第二个成员变量,默认被置为0。对我们理解block的本质无实际意义,不展开讨论。
  • Reserved。Reserved作为结构体的第三个成员变量,是一个保留字段,暂未被使用。对我们理解block的本质无实际意义,不展开讨论。
  • FuncPtr。FuncPtr是一个函数指针,作为结构体的第四个也是最后一个成员变量。这个函数指针用于指向block的定义。Objective-C层面调用block底层就是调用的这个函数指针。

__main_block_impl_0

__main_block_impl_0是用来描述block实现的结构体,这个结构体是编译器根据上下文,动态生成并插入进来的。

这个结构体的命名是有规律的:结构体名称前面的main是指包含block定义的那个函数或方法。结构体名称后面的数字0是指当前这个block是函数内的第几个block。从0开始,此处main函数中就只有1个block,所以动态生成的结构体名称即为__main_block_impl_0。所以这个结构体与通用结构体\__block_impl不同,__main_block_impl_0并非一个通用结构体,Objective-C层面的每一个block在底层都有一个与之对应的用来描述其实现的结构体。当然,从另一个角度:这个结构体是编译器根据上下文,动态生成并插入进来的也可以断定这个结构体的非通用性。 同样,__main_block_impl_0不仅包含一些成员变量,也包含一个构造方法,从这个角度看,__main_block_impl_0更像一个类。本质上,C++中的结构体和类没太大区别。 C++结构体和类题外话:struct和class除了成员变量的访问权限不同,其他都是相同的。就连在内存中的表现都是一模一样的。struct的默认成员访问权限是public;class的默认成员访问权限是private。

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

__main_block_impl_0成员变量和成员函数如下:

  • impl。这是一个__block_impl类型的成员变量。__block_impl作为结构体__main_block_impl_0的第一个成员变量。这很有用,将在下文展开讨论。
  • Desc。这是一个__main_block_desc_0类型的成员变量。__main_block_desc_0将在下文介绍。
  • __main_block_impl_0。这是一个与结构体同名的成员函数,与其说这是一个成员函数,不如说这是一个构造方法。该构造方法和其他语言中的构造方法一样,可以初始化并返回一个实例对象。__main_block_impl_0函数接收两个外部参数(除了flags之外),然后对其成员变量impl和Desc进行配置并返回一个__main_block_impl_0类型的实例对象。通过__main_block_impl_0函数的实现不难看出,该构造函数主要配置了impl的isa指针(指向&_NSConcreteStackBlock,即栈block)impl的Flags使用默认参数设置为0。impl的函数指针FuncPtr指向了外部传递进来的参数fp。至于fp是在哪里传递进来的下文有介绍。

__main_block_desc_0

__main_block_desc_0同样是一个结构体,用来描述block的其他信息,本例中主要包括block的size。同样,__main_block_desc_0也是编译器根据上下文,动态生成并插入进来的。并且和结构体__main_block_impl_0存在同样的命名规律,即__block所在函数_block_impl_block在当前函数中的下标 __main_block_desc_0的定义如下:

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)};
  • reserved。这是一个保留字,目前没有实际意义。
  • Block_size。描述block所占的内存空间大小,对我们理解block的本质也无实际意义。

观察上面代码,紧随__main_block_desc_0的定义之后即声明了一个实例对象__main_block_desc_0_DATA。且该实例对象的reserved被设置为0,Block为size被设置为结构体__main_block_impl_0所占用的内存大小。

__main_block_func_0

__main_block_func_0是一个静态函数。观察其定义,可以看出__main_block_func_0的定义就是Objective-C层面block的定义,所以将来调用__main_block_func_0就相当于调用block的函数体。如下:

static int __main_block_func_0(struct __main_block_impl_0 *__cself, int num1, int num2) {
    return num1 + num2;
}

main 函数

阅读以上C++源码,可以看出main函数的定义在28—34行之间。如下:

int main () {
    int a = 1, b = 2;
    int (*block)(int, int) = ((int (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    int sum = ((int (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, a, b);
    printf("%d\n", sum);
    return 0;
}

第2行声明并定义了两个变量a和b。 第3行代码如下:

int (*block)(int, int) = ((int (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

去掉类型转换对代码进行精简之后如下:

`int (*block)(int, int) = &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);`

实际上就是调用__main_block_impl_0函数实例化了一个名为block的__main_block_impl_0结构体对象,然后通过&符号取结构体对象地址。上面已经说过__main_block_impl_0函数是__main_block_impl_0结构体的构造方法。 __main_block_impl_0函数接收两个指针作为参数。第一个参数传递的是一个函数指针 —__main_block_func_0。学过C语言的应该知道,C语言中的函数名就是函数的地址。所以__main_block_impl_0就是指向这个函数的指针。通过查看__main_block_func_0的定义,可以得知__main_block_func_0的函数实现就是block的函数实现(即调用block要执行的代码块)。至此,我们知道,__main_block_impl_0函数的第一个参数是一个代表block的具体定义的函数。 __main_block_impl_0函数的第二个参数是一个__main_block_desc_0结构体实例,该结构体实例是__main_block_desc_0_DATA。上面已经说过,该结构体目前仅仅描述了block的size。 综上,第3行代码本质上就是实例化一个__main_block_impl_0结构体对象。

回过头再来看__main_block_impl_0结构体的定义:__main_block_impl_0结构体包含两个成员变量impl和Desc和一个与__main_block_impl_0结构体同名的构造函数__main_block_impl_0。其中成员变量impl是一个名称为__block_impl的结构体,impl是__main_block_impl_0的第一个成员变量。结构体__block_impl中主要包括两个指针isa和FuncPtr。__main_block_impl_0的第二个成员变量Desc(__main_block_desc_0类型)也是一个结构体,上面已经说过,__main_block_desc_0仅仅描述了block的size。结构体第三个成员变量是一个构造函数,该构造函数主要对impl这个结构体实例的isa和FuncPtr进行配置。其中isa设置为“&_NSConcreteStackBlock”,说明block的类型是栈block。FuncPtr被设置为外部参数fp。至此,可以得知,第3行调用构造函数初始化block时传递的函数指针__main_block_func_0被设置给了impl结构体的函数指针FuncPtr。

接下来继续分析第4行代码:

int sum = ((int (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, a, b);

通过查看第4行代码,可以看出该行代码是调用block。去除类型转换后,代码可以精简为

int sum = (((__block_impl *)block)->FuncPtr)((__block_impl *)block, a, b);

以上代码,block是第三行__main_block_impl_0函数初始化而来,block本该是__main_block_impl_0类型的实例,这里却被强制转换为了__block_impl类型并且无论是在编译时还是运行时都不会报错也不会访问非法内存地址。归根到底,因为__block_impl是__main_block_impl_0结构体的第一个成员变量,block的内存起始地址就是它的__block_impl类型的成员变量impl的内存地址。换句话说,相当于将block_impl结构体的成员直接拿出来放在main_block_impl_0中,那么也就说明block_impl的内存地址就是main_block_impl_0结构体的内存地址开头。所以可以转化成功。并且可以合法的访问FunPtr。

block 被强转为__block_impl类型,就可以访问FuncPtr函数,block->FuncPtr接收了block、a、b三个参数。还记得FuncPtr这个函数指针的由来吗?FuncPtr就是在第3行中传入的函数指针__main_block_func_0。上面已经说过__main_block_func_0就是block的实现。所以执行block->FuncPtr就相当于执行__main_block_func_0。至此,block的调用结束。

通过以上分析,得知,block的定义本质上就是实例化一个__main_block_impl_0结构体对象。block的调用就是调用这个结构体对象内的成员变量impl的名为FuncPtr的函数指针。其中FuncPtr指针指向了block的实现(即block代码块)

增加注释后的C++源码

// block的底层结构和布局
struct __block_impl {
    void *isa;          // isa是一个指针,指向block所属类型(栈block、堆block等)
    int Flags;
    int Reserved;
    void *FuncPtr;      // FuncPtr是一个函数指针,指向block代码块的定义,即调用block时执行的代码
};

// block的描述信息
static struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
} 
// __main_block_desc_0类型的实例
__main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

// block实现结构体
struct __main_block_impl_0 {
    // block结构体实例
    struct __block_impl impl;
    // block描述
    struct __main_block_desc_0* Desc;
    // 实例化block实现结构体的构造方法
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
        // 设置block类型为栈block
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        // 设置block的函数指针
        impl.FuncPtr = fp;
        // 设置描述信息
        Desc = desc;
    }
};

// block的函数体实现/定义
static int __main_block_func_0(struct __main_block_impl_0 *__cself, int num1, int num2) {
    return num1 + num2;
}

int main () {
    int a = 1, b = 2;
    // 定义结构体实例变量block:构造一个\__main_block_impl_0类型的结构体实例。调用了__main_block_impl_0这个构造函数。
    // 函数__main_block_impl_0接受两个参数(通过__main_block_impl_0结构体及其结构体构造方法的定义也可得知),一个参数是函数指针FuncPtr,此处传递的是__main_block_func_0这个函数,该函数即是block的实现/定义。另一个参数是这个block的描述信息,主要包括block所占空间大小
    int (*block)(int, int) = ((int (*)(int, int))  &__main_block_impl_0  ((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    // 下面是对应的源码中调用block的代码,即int sum = block(a,b);
    // 此处调用的是block->FuncPtr函数,上面已经说过,FuncPtr就是指向函数__main_block_func_0的函数指针调用FuncPtr就相当于调用__main_block_func_0。此处传递的是block、a、b
    int sum = ((int (*)(__block_impl *, int, int))  ((__block_impl *)block)->FuncPtr)  ((__block_impl *)block, a, b);
    printf("%d\n", sum);
    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

总结

通过以上分析,不难得知,block的定义本质上就是实例化一个__main_block_impl_0结构体对象,内部也有一个isa指针,并且这个结构体封装了函数调用以及函数调用相关参数。调用构造方法实例化该结构体对象时会把block的定义作为函数指针传递给结构体内的成员变量impl的FuncPtr。block的调用就是调用这个结构体对象内的成员变量impl的名为FuncPtr的函数指针。其中FuncPtr指针指向了block的实现(即block代码块)。基于以上结论,我们也可以使用C++或者C语言对Objective-C的block做一次精简版实现。大致思路:定义一个block的结构体或类。Objective-C定义block时使用该结构体或类实例化一个blk对象,并把block的实现代码块作为一个函数指针传递给该对象暂存,Objective-C调用block时则直接调用blk对象内暂存的函数指针。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券