Clang 是如何编译Block的?

Posted Haley_Wong

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Clang 是如何编译Block的?相关的知识,希望对你有一定的参考价值。

在开始介绍Clang编译Block之前,可以先了解下Clang编译器:
Xcode clang 编译器 中文
The Compiler 英文原文

上一篇文章说,Block 是“带有自动变量值的匿名函数”,但是看到本文中Clang将源码转换成C++源码后的内容,会发现Block的功能其实是由几个结构体、构造函数、和一个函数完成的。

1.如何生成转换后的源码?

我们先创建一个简单的命令行应用,来看一下最简单的C++函数中中的Block经过Clang转换后是什么样子的。

首先,创建一个MacOS的命令行工程:

然后,在main.m中创建一个block变量,然后执行block。

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) 
    void (^blk)(void) = ^ 
        printf("Block\\n");
    ;
    blk();
    return 0;

为了能够了解编译器对源码的转换过程,我们需要使用到Clang的一些命令。

这里使用clang -rewrite-objc .m文件的命令,将main.m转换一下。
完整的命令:

 clang -rewrite-objc main.m

执行完命令后,就会在main.m所在目录生成main.cpp文件。

然后就可以打开main.cpp来阅读转换后的源码。

小提示
虽然这里转换的是C++代码,但实际上转换OC的.m文件也是一样的命令。

2.转换后的block相关源码分析

因为main.m文件中引入了Foundation框架,即#import <Foundation/Foundation.h>,而Foundation中是有很多的对象,所以这里对象都会被转换成C++源码,所以main.cpp有点长,我们就挑跟Block相关的主要的源码来分析和了解。

main.cpp的前面部分是Foundation框架相关的转换,其中在60行至80就有关于block的一些声明:

#ifndef BLOCK_IMPL
#define BLOCK_IMPL
struct __block_impl 
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
;
// Runtime copy/destroy helper functions (from Block_private.h)
#ifdef __OBJC_EXPORT_BLOCKS
extern "C" __declspec(dllexport) void _Block_object_assign(void *, const void *, const int);
extern "C" __declspec(dllexport) void _Block_object_dispose(const void *, const int);
extern "C" __declspec(dllexport) void *_NSConcreteGlobalBlock[32];
extern "C" __declspec(dllexport) void *_NSConcreteStackBlock[32];
#else
__OBJC_RW_DLLIMPORT void _Block_object_assign(void *, const void *, const int);
__OBJC_RW_DLLIMPORT void _Block_object_dispose(const void *, const int);
__OBJC_RW_DLLIMPORT void *_NSConcreteGlobalBlock[32];
__OBJC_RW_DLLIMPORT void *_NSConcreteStackBlock[32];
#endif
#endif
#define __block
#define __weak

这里就先简单了解一下:

  1. __block_impl就是Block结构体,内部四个参数:
  • isa存储Block的类型,它的值一般就是下面声明的_NSConcreteGlobalBlock_NSConcreteStackBlock。其实还有一个是_NSConcreteMallocBlock
  • Flags存一些标志的,目前没什么用,默认都是0。
  • Reserved 存以后版本升级所需的区域值。目前也没什么用,默认是0。
  • FuncPtr 存block执行时对应的函数指针,后面会讲到。
  1. _Block_object_assign_Block_object_dispose是做block的拷贝和block释放的函数。(以后其他文章会讲到)
  2. _NSConcreteGlobalBlock_NSConcreteStackBlock就是Block的类型,就像一般变量的Class。实际上OC中Block是根据其存储区域来定义的,一共有三种类型:_NSConcreteGlobalBlock_NSConcreteStackBlock_NSConcreteMallocBlock。分别代表全局Block、栈区Block、堆区Block。这也与Block以及其捕获的变量的存储区域有关系。

以上这些关于Block的声明应该是属于Foundation框架内的,不管你是否使用Block,只要你导入了Foundation框架,都会包含。

在最后98000多行,就是main函数转换后的C++源码了。

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 void __main_block_func_0(struct __main_block_impl_0 *__cself) 

        printf("Block\\n");
    

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[]) 
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;

看到这些源码,可以了解到Block语法被转换成了几个结构体和一个包含Block中中执行内容的函数。

2.1 用于存储block存储的结构体

这里首先声明了一个用来描述Block实现的结构体__main_block_impl_0。实际上这个结构体是编译器根据上下文,动态生成并插入进来的。

它的结构体名称规则与Block所处的函数略有不同:

  1. 如果是在C++函数中,命名规则是:Block所在函数的函数名 + _block_impl_+索引(目前block是该函数中的第几个block)。例如,这里Block所在函数名是main,该block是该函数内的第一个block,因为名称叫__main_block_impl_0
  2. 如果是在OC函数中,则命名规则是 类名 + 函数名+ __block_impl + 索引(目前block是该函数中的第几个block)。例如:__HLObject__objctTest_age__block_impl_1
  3. 该结构体中有两个变量impl*Desc,以及一个构造函数。impl 就是上面介绍的__block_impl结构体,而*Desc则是描述Block实现的结构体变量指针。

2.2 block执行函数

static void __main_block_func_0(struct __main_block_impl_0 *__cself) 

        printf("Block\\n");
    

这里就很简单了,这是封装了一个用来执行block内部代码的函数。函数名也是与__main_block_impl_0的生成规则类似。

  1. 如果是在C++函数中,命名规则是:Block所在函数的函数名 + _block_func_ + 索引(目前block是该函数中的第几个block)。
  2. 如果在OC函数中,则命名规则是 类名 + 函数名 + _block_func_ + 索引(目前block是该函数中的第几个block)。例如:__HLObject__objctTest_age__block_func_2
  3. 参数也就是 2.1 中介绍的__main_block_impl_0结构体变量。
  4. 注意这是一个静态函数。

2.3 用于描述block的结构体

例如:__main_block_desc_0,它内部就只有两个变量reservedBlock_sizereserved是用来存以后版本升级所需的区域值,目前没有什么作用,就填一个默认值0;Block_size使用来存block的占用内存大小的。

并且这里已经定义了一个描述block的结构体的静态变量__main_block_desc_0_DATA

而该结构体类型的名称命名规则也与上面的规则类似。

  1. 如果是在C++函数中,命名规则是:Block所在函数的函数名 + _block_desc_ + 索引(目前block是该函数中的第几个block)。
  2. 如果在OC函数中,则命名规则是 类名 + 函数名 + _block_desc_ + 索引(目前block是该函数中的第几个block)。例如:__HLObject__objctTest_age__block_desc_1

2.4 含有block的函数

最后,该介绍main.m被转换后的源码了。

int main(int argc, const char * argv[]) 
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;

第一行,是定义一个blk的变量,由于做了太多转换,看起来不方面,这里先做一下分解处理:

// 先构造一个`__main_block_impl_0`变量
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);

// 将该变量地址赋值给blk
struct __main_block_impl_0 *blk = &tmp;

这里第一步到__main_block_impl_0的内部构造函数,其实赋值过程大概是这样的:

impl.isa = &_NSConcreteStackBlock;
impl.Flags = 0;
impl.Reserved = 0;
impl.FuncPtr = fp;
Desc = &__main_block_desc_0_DATA;

接下来就到了调用block的部分了。

((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

这其实就是blk(),转换出来了,因为涉及到了比较多的类型转换。
如果去掉一些类型转换和省略掉的部分。该源码应该是这样的:

(*blk->impl.FuncPtr)(blk);

这其实就是很简单的使用函数指针来调用函数。前面创建的blk变量,其实已经将__main_block_func_0函数的指针赋值到了成员变量impl的FuncPtr中了。所以这里通过这个函数指针将函数取出来调用,并再次将blk作为参数传进去。

最后,附上使用OC中的函数经过Clang转换出源码的前后对比。
比如自定义一个HLObject对象,以及一个- (void)objctTest:(NSString *)name age:(int)age函数。

#import "HLObject.h"

@implementation HLObject

- (void)objctTest:(NSString *)name age:(int)age

    void (^blk)(void) = ^ 
        printf("Block\\n");
    ;
    blk();


@end

经Clang转换后的源代码:

// @implementation HLObject


struct __HLObject__objctTest_age__block_impl_0 
  struct __block_impl impl;
  struct __HLObject__objctTest_age__block_desc_0* Desc;
  __HLObject__objctTest_age__block_impl_0(void *fp, struct __HLObject__objctTest_age__block_desc_0 *desc, int flags=0) 
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  
;
static void __HLObject__objctTest_age__block_func_0(struct __HLObject__objctTest_age__block_impl_0 *__cself) 

        printf("Block\\n");
    

static struct __HLObject__objctTest_age__block_desc_0 
  size_t reserved;
  size_t Block_size;
 __HLObject__objctTest_age__block_desc_0_DATA =  0, sizeof(struct __HLObject__objctTest_age__block_impl_0);

static void _I_HLObject_objctTest_age_(HLObject * self, SEL _cmd, NSString *name, int age) 
    void (*blk)(void) = ((void (*)())&__HLObject__objctTest_age__block_impl_0((void *)__HLObject__objctTest_age__block_func_0, &__HLObject__objctTest_age__block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);


// @end

总结:Block语法的转换结果如下:
block实现的结构体、描述block的结构体、block执行内容的函数。构造一个block实现的结构体变量。

那这些结构体和参数的命名规则如下:

  1. 如果是在C++函数中,命名规则是:两个下划线 + Block所在函数的函数名 + 一个下划线 +block_impl_ + 索引(目前block是该函数中的第几个block)。
  2. 如果在OC函数中,则命名规则是 两个下划线 + 类名 + 两个下划线 + 函数名 + 两个下划线 +block_impl_ + 索引(目前block是该函数中的第几个block)。
  3. 将上面的block_impl_替换成block_desc就是描述block的结构体了。如果替换成block_func_就是block执行的函数名了。

有意思的是,我们竟然可以自定义一个编译器帮我们转换出的block结构体类型的变量,例如这样:

        int a = 0;
        void (^block)(void) = ^
            printf("block---%d", a);
        ;
        NSLog(@"block--class:%@", [block class]);
        
        struct __main_block_imp_0 *block_struct = (__bridge struct __main_block_imp_0 *)block;
        
        block();

所以说,Block其实是一堆结构体+一个函数。但是其他也是一个Objective-C对象,因为其内存存储着一个isa执行其Class的指针。

以上是关于Clang 是如何编译Block的?的主要内容,如果未能解决你的问题,请参考以下文章

Block深入浅出

每次我尝试使用`clang -Weverything`进行编译时,都会收到警告“包含位置'/usr/local/include'对于交叉编译不安全”

如何通过 .xcconfig 禁用特定的 clang 诊断?

iOS开发面试拿offer攻略之block篇

格而知之16:我所理解的Block

Block中是如何实现截获自动变量值的呢?