Swift底层探索方法介绍
在探索Swift一些底层原理的时候,常常会无从下手,分享几个可行的路子。
基础
- Swift源码定义了写swift程序的基本功能层,如基本的数据类型,通用的数据结构,全局函数,常用的抽象协议等。我们也可以通过研究标准库的代码来学习swift中各类数据结构和方法的底层实现。
- iOS开发的语言不管是OC还是Swift都是通过LLVM进行编译的。如下图所示:
swift的前端编译器为swiftc。一个Swift文件的编译从编译器的角度会经过如下步骤。
- Code经过词法分析、语法分析生成AST抽象语法树
- AST通过SILGen生成原生SIL文件(代码量很大,没有进行类型检查等)
- 原生SIL文件优化生成最终SIL文件
- 该SIL文件通过IRGen生成IR
- IR生成机器码
注:生成可执行文件的流程中,swiftc和clang的区别就在于swiftc多了生成SIL文件这一步。
方案说明
swiftc
在上述介绍的swift编译流程中,我们可以通过swiftc -h
查看常用指令
perl
MODES:
-dump-ast 解析和类型检查源文件 & 转换成 AST
-dump-parse 解析源文件 & 转换成 AST
-emit-assembly 生成汇编文件
-emit-bc 生成 LLVM Bitcode 文件
-emit-executable 生成已链接的可执行文件
-emit-imported-modules 生成已导入的库
-emit-ir 生成 LLVM IR 文件
-emit-library 生成已连接的库
-emit-object 生成目标文件
-emit-silgen 生成 raw SIL 文件(第一个阶段)
-emit-sil 生成 canonical SIL 文件(第2个阶段)
-index-file 为源文件生成索引数据
-print-ast 解析和类型检查源文件 & 转换成更简约的格式更好的 AST
-typecheck 解析和类型检查源文件
···
就可读性而言,我们常选择sil文件来进行分析。以下主要对sil文档的分析进行简单介绍。如果想查看更多信息,可以查看官方文档。
IR基本语法
rust
// 数组
[<elementnumber> x <elementtype>]
// example
alloca [24 x i8], align 8 24个i8都是0
// 结构体
%swift.refcounted = type { %swift.type*, i64 }
//表示形式
%T = type {<type list>} //这种和C语⾔的结构体类似
// 指针类型
<type> *
//表示形式
i64* //64位的整形
// getelementptr指令:获取数组和结构体的成员
<result> = getelementptr <ty>, <ty>* <ptrval>{, [inrange] <ty> <id x>}*
<result> = getelementptr inbounds <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*
sil文件常见指令
@main
:入口【sil中的标识符名称以@为前缀】%1
,%2
:sil中的寄存器(常量),一旦赋值不可修改alloc_global
:分配一个全局变量metatype
:获取元类型function_ref
:获取函数指针apply
:调用方法alloc_ref
:分配一个类型为T的实例对象,默认的引用计数为1,也就是在堆上分配内存空间alloc_stack
:在栈区为变量分配内存空间integer_literal
:构建一个整形变量(整形变量在swift中其实为结构体类型。)store {A} to {B}
:将A存储到指定变量B中
注:在SIL文件中常常可以看到类似
s4main6personAA11PandaPersonCvp
这样经过混淆的变量名,可以通过xcrun swift-demangle s4main6personAA11PandaPersonCvp
指令来将其还原。
寄存器
在sil文件中可以看到许多%0,%1,代表的是寄存器。请注意这里的寄存器和我们平时研究编译原理中的寄存器不太一样。 我们在工程中打开调试模式并开启Always Show Dissasembly
之后通过register read
命令拿到的这些寄存器信息中的寄存器是不同的。如下图所示,这些寄存器是真实存在的。
而我们的sil文件中的寄存器是虚拟的。我们可以理解为常量【一旦赋值无法改变】。运行的时候会对应到真实的寄存器。
Example
首先我们创建一个main.swift文件
ini
class PandaPerson {
var name: String = "Letty"
var age: Int = 18
}
let person = PandaPerson()
我们现在要探索的就是let person = PandaPerson()
这句代码背后做了什么事情。类比OC创建对象,alloc
内存分配,init
初始化操作。而Swift提供了这样默认的初始化器,我们将通过SIL文件来探索。
css
# 执行下述命令生成sil文件
swiftc -emit-sil main.swift > main.sil
首先我们可以找到入口函数@main
swift
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32
下面具体看看@main
函数中在创建对象部分做了什么事
less
// 1. 分配一个全局变量person
alloc_global @$s4main6personAA11PandaPersonCvp
// 2. 为该变量分配内存空间(即创建实例对象)
%3 = global_addr @$s4main6personAA11PandaPersonCvp : $*PandaPerson
// 3. 获取PandaPerson元类型
%4 = metatype $@thick PandaPerson.Type
// 4. 获取PandaPerson的__allocating_init()函数指针
%5 = function_ref @$s4main11PandaPersonCACycfC : $@convention(method) (@thick PandaPerson.Type) -> @owned PandaPerson
// 5. 调用__allocating_init()方法,将方法返回值存至%6
%6 = apply %5(%4) : $@convention(method) (@thick PandaPerson.Type) -> @owned PandaPerson
// 6. 将创建的对象存储到第三步中为全局变量person分配的内存空间中
store %6 to %3 : $*PandaPerson
···
swift源码
仓库地址: github.com/apple/swift
目录结构
less
graph TD
A(swift-main) --> B("docs:文档")
A --> C("stdlib:swift源码")
A --> D("lib:c++源码")
A --> E("include:c++头文件")
A --> I("···")
常用索引
-
标准库:./stdlib/public/core
- map、filter:./stdlib/public/core/Sequence.swift
- flatMap、compactMap、reduce:./stdlib/public/core/SequenceAlgorithms.swift
- Substring:./stdlib/public/core/Substring.swift
- Optional:./stdlib/public/core/Optional.swift
-
MetaData:./include/swift/ABI/MetaData.h
总结
SIL文件可以帮助我们探究方法调用流程和部分数据结构的底层实现,swift源码则更贴近开发者,帮助我们了解swift对于数据和方法等设计及其原理。具体问题具体分析,笔者建议两者可以结合使用。