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
这里就先简单了解一下:
__block_impl
就是Block结构体,内部四个参数:
isa
存储Block的类型,它的值一般就是下面声明的_NSConcreteGlobalBlock
、_NSConcreteStackBlock
。其实还有一个是_NSConcreteMallocBlock
。Flags
存一些标志的,目前没什么用,默认都是0。Reserved
存以后版本升级所需的区域值。目前也没什么用,默认是0。FuncPtr
存block执行时对应的函数指针,后面会讲到。
_Block_object_assign
和_Block_object_dispose
是做block的拷贝和block释放的函数。(以后其他文章会讲到)_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所处的函数略有不同:
- 如果是在C++函数中,命名规则是:Block所在函数的函数名 +
_block_impl_
+索引(目前block是该函数中的第几个block)。例如,这里Block所在函数名是main
,该block是该函数内的第一个block,因为名称叫__main_block_impl_0
。 - 如果是在OC函数中,则命名规则是 类名 + 函数名+
__block_impl
+ 索引(目前block是该函数中的第几个block)。例如:__HLObject__objctTest_age__block_impl_1
。 - 该结构体中有两个变量
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
的生成规则类似。
- 如果是在C++函数中,命名规则是:Block所在函数的函数名 +
_block_func_
+ 索引(目前block是该函数中的第几个block)。 - 如果在OC函数中,则命名规则是 类名 + 函数名 +
_block_func_
+ 索引(目前block是该函数中的第几个block)。例如:__HLObject__objctTest_age__block_func_2
。 - 参数也就是 2.1 中介绍的
__main_block_impl_0
结构体变量。 - 注意这是一个静态函数。
2.3 用于描述block的结构体
例如:__main_block_desc_0
,它内部就只有两个变量reserved
和Block_size
,reserved
是用来存以后版本升级所需的区域值,目前没有什么作用,就填一个默认值0;Block_size
使用来存block的占用内存大小的。
并且这里已经定义了一个描述block的结构体的静态变量__main_block_desc_0_DATA
。
而该结构体类型的名称命名规则也与上面的规则类似。
- 如果是在C++函数中,命名规则是:Block所在函数的函数名 +
_block_desc_
+ 索引(目前block是该函数中的第几个block)。 - 如果在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实现的结构体变量。
那这些结构体和参数的命名规则如下:
- 如果是在C++函数中,命名规则是:
两个下划线
+Block所在函数的函数名
+一个下划线
+block_impl_
+索引
(目前block是该函数中的第几个block)。 - 如果在OC函数中,则命名规则是
两个下划线
+类名
+两个下划线
+函数名
+两个下划线
+block_impl_
+索引
(目前block是该函数中的第几个block)。 - 将上面的
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的?的主要内容,如果未能解决你的问题,请参考以下文章
每次我尝试使用`clang -Weverything`进行编译时,都会收到警告“包含位置'/usr/local/include'对于交叉编译不安全”