「这是我参与2022首次更文挑战的第11天,活动详情查看:2022首次更文挑战」
iOS开发语言,不论是OC还是Swift,都是通过LLVM进行编译的,最终生成.o文件,其编译流程如下图:
OC通过clang编译器,编译成IR,然后再生成可执行文件.o(也就是我们的机器码);Swift则是通过Swift编译器生成IR,然后再生成可执行文件.o;
Swift编译流程
Swift语言编译流程图如下:
Swift代码经过-dump-parse命令进行语法分析,生成抽象语法树AST;抽象语法树通过-dump-ast进行语义分析(比如类型检查是否正确,是否安全);语义分析之后,Swift代码将会降级为SIL,也就是Swift中间语言(Swift intermediate language);SIL分为Raw SIL(原生的,没有开启优化选项)和SILOpt Canonical SIL(经过优化的);- 最终通过
LLVM降级为IR,然后通过后段代码编译为不同架构的机器码;
流程中涉及的命令如下:
// 分析输出AST
swiftc main.swift -dump-parse
// 分析并且检查类型输出AST
swiftc main.swift -dump-ast
// 生成中间体语言(SIL),未优化
swiftc main.swift -emit-silgen
// 生成中间体语言(SIL),优化后的
swiftc main.swift -emit-sil
// 生成LLVM中间体语言 (.ll文件)
swiftc main.swift -emit-ir
// 生成LLVM中间体语言 (.bc文件)
swiftc main.swift -emit-bc
// 生成汇编
swiftc main.swift -emit-assembly
// 编译生成可执行.out文件
swiftc -o main.o main.swift
相对于OC,在Swift的编译过程中多了SIL,SIL会对我们的代码进行安全检查,比如如下代码:
在OC中:
我们定义的int8_t类型的计算结果将会出错,因为int8_t只有 一个字节,其运算结果已经溢出,在OC中给出了一个错误的运算结果;我们再来看在Siwft中会发生什么:
我们看到,同样的代码,在Swift中的编译阶段就会报错,避免后续发生不可预知的错误;这些都是因为SIL存在的结果;
SIL文件分析
那么SIL究竟有着什么样的语法规则呢?我们来生成一个SIL文件进行分析;我们先写一段简单的代码:
接下来我们将这段代码生成SIL文件:
为了方便观看,我们也可以通过swiftc main.swift -emit-sil > ./main.sil指令将SIL文件输出为main.sil文件;
由于SIL文件内容较多,我们只选择重要的部分分析:
Teacher的声明
首先,我们先看一下Teacher在SIL阶段的声明部分:
从Teacher的定义中可以看到有初始化的存储属性age和name,还有一个标识为@objc的deinit函数,以及默认的初始化器init();
main函数
@main:标识入口函数,在SIL中@是作为标识符的;%0--%9:这写在SIL中也被称为寄存器,可以理解为开发过程中的常量,一旦赋值,将无法再次修改(所以后边数字会一直累加);需要注意的是,此处的寄存器是虚拟寄存器,最终运行到具体的设备上时,会使用真实的寄存器;alloc_global @$s4main1tAA7TeacherCvp:分配一个全局变量,该全局变量的名称是混写的,我们可以通过终端指令xcrun swift-demangle将其还原:
可以看到此处对应的是main文件中的t变量,对应的是main中的Teacher;
%3 = global_addr @$s4main1tAA7TeacherCvp:拿到该全局变量的地址给%3;%4 = metatype $@thick Teacher.Type:获取Teacher.Type的元类型给%4;- 根据注释可以知道
%5是Teacher.__allocating_init()函数的引用,也就是其指针地址; %6 = apply %5(%4):将%5(%4)的结果,也就是Teacher的实例变量,赋值给%6;store %6 to %3:将%6也就是实例变量的内存地址,存放到%3这个全局变量中;- 在
Swift底层中Int就是一个Struct类型,%8和%9是在构建一个Int32的整数类型0,类似与我们OC中main函数最终的return 0;
关于SIL语法规则,请查看SIL官方文档
__allocating_init()函数
在上述SIL代码中调用了s4main7TeacherCACycfC也就是Teacher.__allocating_init()函数来创建当前的实例对象的;我们在SIL中定位到该函数:
- 该函数需要一个
Teacher.Type的元类型,我们可以将此元类型理解为isa; %1 = alloc_ref $Teacher:alloc_ref会创建一个Teacher的实例变量,其引用计数初始化为1;alloc_ref实际上也就是去堆区申请内存空间;如果标识为objc的Swift类将会使用Objective-C的+allocWithZone:初始化方法;如何验证呢?我们在代码中添加如下断点:
运行程序,查看汇编指令:
我们看到将会调用Teacher.__allocating_init()函数,那么该函数是如何实现的呢?断点进入该函数:
在__allocating_init()函数中主要调用了swift_allocObject和Teacher.init();
swift_allocObject在堆区找到合适的内存空间初始化;Teacher.init()初始化成员变量;
这是一个纯粹的Swift类,那么如果我们将Teacher继承自NSObject会发生什么呢?
我们重复上述汇编调试步骤,进入Teacher.__allocating_init()函数:
我们发现,初始化函数变成了objc_allocWithZone以及objc_msgSend;
objc_allocWithZone调用malloc函数申请内存空间;objc_msgSend发送init消息;
swift_allocObject
在前边我们已经分析出Swift类初始化过程中会调用swift_allocObject,那么该函数做了什么呢?我们需要借助于Swift源码进行分析;
在该目录下找到stdlib->public->runtime->HeapObject.cpp文件,该文件是和我们的Swift类初始化相关的文件;在该文件中定位到_swift_allocObject_函数,这是一个私有函数,其是被swift::swift_allocObject调用起来的:
_swift_allocObject_实现如下:
该函数有三个参数:
HdapMetadata const *metadata:元数据类型;requiredSize:所需要的大小;requiredAlignmentMask:对齐所需要的掩码,可以从objc的源码中得知,其为7,因为是8字节对齐;
将requiredSize和requiredAlignmentMask传递给函数swift_slowAlloc,该函数返回了一个HeapObject类型的指针; reinterpret_cast用来做指针类型的转换;
new (object) HeapObject(metadata):HeapObject初始化;
那么swift_slowAlloc是用来干什么的呢?
swift_slowAlloc
其实现如下:
在swift_slowAlloc函数中,调用了malloc函数来开辟内存空间;
流程总结
那么,我们大致可以总结出Swift对象进行内存分配的流程:
- 首先会调用
_allocating_init():该函数有编译器生成; - 对于纯
Swift类将会再调用swift_allocObject()函数; - 然后在
swift_allocObjec()总会调用私有函数_swift_allocObject; - 然后通过函数
swift_slowAlloc调用malloc来申请堆区的内存空间;