利用Clang探究block的本质

前言

block作为Objective-C语言中的一种特殊的存在,已经为大家所熟知。在其他语言中,也有类似于block的实现,比如JavaScript和Swift中的闭包,python、C++中的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对象内暂存的函数指针。

下一篇文章将探讨《利用Clang探究__block的本质》,持续更新,敬请期待...

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • Blocks Blocks Blocks 是带有局部变量的匿名函数 截取自动变量值 int main(){ ...
    南京小伙阅读 882评论 1 3
  • Block在iOS开发中的用途非常广,今天我们就来一起探索一下Block的底层结构。 1. Block的底层结构 ...
    雪山飞狐_91ae阅读 3,877评论 16 28
  • 1.Block的实现 我们在命令行下输入clang -rewrite-objc 源代码文件名就可以把含有block...
    雪山飞狐_91ae阅读 361评论 0 2
  • 摘要block是2010年WWDC苹果为Objective-C提供的一个新特性,它为我们开发提供了便利,比如GCD...
    西门吹雪123阅读 882评论 0 4
  • 今天我们作文班进行了一场五子棋比赛。在比赛过程中,我们组的竞争非常激烈,可是第一名还是故事大王那一组。 ...
    余叚阅读 283评论 0 1