前言
本篇文章会大致分析一下swift的编译流程
,这个作为了解
即可,然后会重点
分析一下swift中类的结构
,这个知识点我们需要掌握
。
一、swift编译流程
1.1 LLVM
在了解swift编译流程之前,我们最好清楚
LLVM是什么
?请参考LLVM编译流程
LLVM是架构编译器的框架系统
,以C++
编写而成,用于优化
任意程序语言编写的程序的编译时间(compile-time)
、链接时间(link-time)
、运行时间(run-time)
以及空闲时间(idle-time)
。对开发者保持开放,并兼容已有脚本。
对于我们iOS系统,OC语言
前端使用Clang编译器
,而swift语言
前端使用swift编译器
,这两个编译器将我们写的代码编译生成IR中间代码
,交给LLVM优化器
进行优化,接着交给代码生成器
生成机器语言,最终形成.o机器执行文件
。整个过程如下图
1.2 swift的编译详细流程
接着我们再详细看看swift的编译流程
,如下图
大致分为以下几步
- swift源码经过
parse解析
、ast编译
,生成AST语法树
。相关指令
swiftc -dump-ast LGPerson.swift >> ast.swift
- 通过
SIL生成器
生成SIL源码
。相关指令
swiftc -emit-sil LGPerson.swift >> ./LGPerson.sil
- 生成
IR中间代码
。相关指令
swiftc -emit-ir LGPerson.swift >> ir.swift
- 输出
.o机器文件
。相关指令
swiftc -emit-object LGPerson.swift
1.3 示例查看编译流程
在示例查看之前,我们可以在【终端】中swiftc -h
查看swiftc的所有相关指令
常用的一些指令的含义
-dump-ast 语法和类型检查,打印AST语法树
-dump-parse 语法检查,打印AST语法树
-dump-pcm 转储有关预编译Clang模块的调试信息
-dump-scope-maps
Parse and type-check input file(s) and dump the scope map(s)
-dump-type-info Output YAML dump of fixed-size types from all imported modules
-dump-type-refinement-contexts
Type-check input file(s) and dump type refinement contexts(s)
-emit-assembly Emit assembly file(s) (-S)
-emit-bc 输出一个LLVM的BC文件
-emit-executable 输出一个可执行文件
-emit-imported-modules 展示导入的模块列表
-emit-ir 展示IR中间代码
-emit-library 输出一个dylib动态库
-emit-object 输出一个.o机器文件
-emit-pcm Emit a precompiled Clang module from a module map
-emit-sibgen 输出一个.sib的原始SIL文件
-emit-sib 输出一个.sib的标准SIL文件
-emit-silgen 展示原始SIL文件
-emit-sil 展示标准的SIL文件
-index-file 为源文件生成索引数据
-parse 解析文件
-print-ast 解析文件并打印(漂亮/简洁的)语法树
-resolve-imports 解析import导入的文件
-typecheck 检查文件类型
首先,我们创建一个swift demo项目,定义一个类LGPerson.swift类
class LGPerson {
var age: Int = 18
var name: String = "luoji"
}
let t = LGPerson()
接着,打开【终端】,进入到项目的目录
- 查看抽象语法树:
swiftc -dump-ast LGPerson.swift
-
生成SIL文件:swiftc -emit-sil LGPerson.swift >> ./LGPerson.sil
可以使用VSCode打开它看看代码量巨多
附:可以在.zshrc
中做了如下配置,这样就能在终端中指定软件打开相应文件
// 打开.zshrc,如果提示打不开,则使用touch .zshrc 创建一个
$ open .zshrc
// 添加以下别名
alias vscode='/Applications/Visual\ Studio\ Code.app/Contents/Resources/app/bin/code'
// 如果之前是创建的,那么还需执行
source .zshrc
//使用
$ swiftc -emit-sil LGPerson.swift >> ./LGPerson.sil && vscode LGPerson.sil
- 如果想SIL文件高亮,需要安装插件:
VSCode SIL
再次打开LGPerson.sil
没有那么全白了
观察LGPerson.sil
代码,可以发现很多都是经过混淆处理的,可以通过下面的指令反混淆
xcrun swift-demangle 你的混淆的代码
1.4 SIL分析
SIL: Swift intermediate language
--> swift中间语言
1.4.1 main函数入口
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer>>):
alloc_global @$s8LGPerson1tA2ACvp // id: %2
%3 = global_addr @$s8LGPerson1tA2ACvp : $*LGPerson // user: %7
%4 = metatype $@thick LGPerson.Type // user: %6
// function_ref LGPerson.__allocating_init()
%5 = function_ref @$s8LGPersonAACABycfC : $@convention(method) (@thick LGPerson.Type) -> @owned LGPerson // user: %6
%6 = apply %5(%4) : $@convention(method) (@thick LGPerson.Type) -> @owned LGPerson // user: %7
store %6 to %3 : $*LGPerson // id: %7
%8 = integer_literal $Builtin.Int32, 0 // user: %9
%9 = struct $Int32 (%8 : $Builtin.Int32) // user: %10
return %9 : $Int32 // id: %10
} // end sil function 'main'
@main
标识当前LGPerson.swift文件的入口函数
,SIL标识符号名称以@作为前缀
。%0,%1...
在SIL中也叫寄存器
,类似代码中的常量
,一旦赋值后不可修改。如果SIL中还要继续使用,就需要使用新的寄存器。
我们一句句的看
-
alloc_global @$s8LGPerson1tA2ACvp
s8LGPerson1tA2ACvp
反混淆出来就是LGPerson
,那么就是创建全局变量LGPerson
-
%3 = global_addr @$s8LGPerson1tA2ACvp : $*LGPerson
这句很简单,读取全局变量LGPerson地址,赋值给%3 -
%4 = metatype $@thick LGPerson.Type
读取LGPerson的Type,赋值给%4 -
%5 = function_ref @$s8LGPersonAACABycfC : $@convention(method) (@thick LGPerson.Type) -> @owned LGPerson
定义一个function_ref即函数,就是%5,这个函数入参是LGPerson.Type -
%6 = apply %5(%4) : $@convention(method) (@thick LGPerson.Type) -> @owned LGPerson
apply调用函数%5,入参就是%4,将返回结果赋给%6 -
store %6 to %3 : $*LGPerson
将%6的结果存储到%3,%3是LGPerson的地址 -
%8 = integer_literal $Builtin.Int32, 0
和%9 = struct $Int32 (%8 : $Builtin.Int32)
就是构建一个Int值,最终返回return %9 : $Int32
综上所述,main函数就是构建了一个全局变量LGPerson
,并对其Type值
做了一个返回处理,那么,这个Type代表什么意思呢?后面我们会仔细分析swift类的底层结构。
1.4.2 实例化相关代码
// LGPerson.__allocating_init()
sil hidden [exact_self_class] @$s8LGPersonAACABycfC : $@convention(method) (@thick LGPerson.Type) -> @owned LGPerson {
// %0 "$metatype"
bb0(%0 : $@thick LGPerson.Type):
%1 = alloc_ref $LGPerson // user: %3
// function_ref LGPerson.init()
%2 = function_ref @$s8LGPersonAACABycfc : $@convention(method) (@owned LGPerson) -> @owned LGPerson // user: %3
%3 = apply %2(%1) : $@convention(method) (@owned LGPerson) -> @owned LGPerson // user: %4
return %3 : $LGPerson // id: %4
} // end sil function '$s8LGPersonAACABycfC'
同样一句句的看
-
%1 = alloc_ref $LGPerson
读取LGPerson的alloc_ref
方法地址,给%1 -
%2 = function_ref @$s8LGPersonAACABycfc : $@convention(method) (@owned LGPerson) -> @owned LGPerson
读取LGPerson.init()函数地址,给%2 -
%3 = apply %2(%1) : $@convention(method) (@owned LGPerson) -> @owned LGPerson
调用alloc_ref
创建一个LGPerson实例对象,给%3 -
return %3 : $LGPerson
返回%3的实例对象
这个__allocating_init()
也不难,其实就是一个简单的方法调用过程。对于这个SIL代码,我们平时可以自己多看多分析
,习惯就好了,哈哈!
二、swift类的结构
带着上面的问题LGPerson中的Type 就是存储的是什么值,代表什么意思?
我们现在来看看swift的类class在底层中的结构是什么样的。
大家都知道,如果要创建一个对象,OC 与 swift的写法是这样的
- OC:
[[LGPerosn alloc] init]
一般alloc
申请内存空间并创建对象,init
对其进行统一初始化处理。 - Swift:
LGPerson()
直接()
就完成了对象的创建。
2.1 找入口
我们现在就来看看LGPerson()
在汇编层是调用了哪些函数?
新建一个SwiftDemo项目工程,在ViewController.swift中添加下面的代码,并打上断点
运行项目,查看汇编
我们找到了对象的初始化入口函数__allocating_init()
,接着我们在__allocating_init()
这一行打上断点,按住control按键 stepinto
进入查看
定位到了swift_allocObject
,同理还是在swift_allocObject
这行加上断点,stepinto
进入查看
没找到实用的信息,那么换种思路,添加符号断点swift_allocObject
,再次run
接着,添加符号断点swift_slowAlloc
最终来到了比较熟悉的malloc_zone_malloc
。
至此,swift实例对象的创建流程
__allocating_init -> swift_allocObject -> _swift_allocObject_ -> swift_slowAlloc -> malloc_zone_malloc
2.2 查看源码验证流程
首先,用VSCode打开swift源码项目,搜索__allocating_init
,打上断点
接着,run项目,在终端输入代码
class LGPerson {
var age: Int = 18
var name: String = "luoji"
}
回车,再输入var t = LGPerson()
,会触发断点
我们可以看左上角,发现requiredSize空间大小
值是40,requiredAlignmentMask字节对齐
值是7。
接着我们顺着流程来到swift_slowAlloc
然后返回申请空间后的地址,即返回指针p。再回到_swift_allocObject_
,那么接着执行
-
auto object = reinterpret_cast
( swift_slowAlloc(requiredSize, requiredAlignmentMask));
通过reinterpret_cast
将指针p转换成HeapObject
类型,然后执行 -
new (object) HeapObject(metadata);
因为object是强转的HeapObject类型,其里面值仍是一个指向内存空间的对象指针
,所以这里执行HeapObject(metadata)
做一个初始化
操作,这样object就是真正的HeapObject结构了。
整体流程图
2.2.1 类的大小
接下来我们具体看看,类的大小size是怎么算出来的?
首先,我们先在lldb中打印查看一下大小,打开Xcode,新建一个swift项目,加入代码
import UIKit
class LGPerson {
var age: Int = 18
var name: String = "luoji"
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
print("Int32 大小: \(MemoryLayout.size)")
print("Int64 大小: \(MemoryLayout.size)")
print("Int 大小: \(MemoryLayout.size)")
print("Srtring 大小: \(MemoryLayout.size)")
print("LGPerson 大小: \(class_getInstanceSize(LGPerson.self))")
}
}
run
我们通过MemoryLayout
得知Int大小是8
,String大小是16
,但是通过class_getInstanceSize
得知类LGPerson
的大小却是40
,40-16-8=16
,这额外的16字节
大小是什么?
之前我们在源码调试的时候,也发现类LGPerson
初始化实例对象时的requiredSize空间大小
值也是40,如下图
额外的16字节从何而来?
我们进入源码工程,查看HeapObject
结构体
结构体大小主要看成员变量,HeapObject结构体有两个成员变量元数据metadata
和 引用计数refCounts
。
metadata
是 HeapMetedata类型,还是查看源码
template struct TargetHeapMetadata;
using HeapMetadata = TargetHeapMetadata;
template
struct TargetHeapMetadata : TargetMetadata {
using HeaderType = TargetHeapMetadataHeader;
TargetHeapMetadata() = default;
constexpr TargetHeapMetadata(MetadataKind kind)
: TargetMetadata(kind) {}
#if SWIFT_OBJC_INTEROP
constexpr TargetHeapMetadata(TargetAnyClassMetadata *isa)
: TargetMetadata(isa) {}
#endif
};
TargetHeapMetadata->TargetMetadata
,也是struct结构体。接着看TargetMetadata
最后我们看看StoredPointer
源码,发现搜不到,但是根据注释
/// The kind. Only valid for non-class metadata; getKind() must be used to get
/// the kind value.
StoredPointer Kind;
我们发现, 可以通过 getKind()
方法入参知道
/// Get the metadata kind.
MetadataKind getKind() const {
return getEnumeratedMetadataKind(Kind);
}
/// Try to translate the 'isa' value of a type/heap metadata into a value
/// of the MetadataKind enum.
inline MetadataKind getEnumeratedMetadataKind(uint64_t kind) {
if (kind > LastEnumeratedMetadataKind)
return MetadataKind::Class;
return MetadataKind(kind);
}
getKind
是调用getEnumeratedMetadataKind
,而入参kind
是uint64_t
类型,占8字节
大小,所以元数据metadata
是8字节大小
。
MetadataKind
这时我们注意到,getKind()
返回值是MetadataKind
类型,我们进入源码看看,里面有一个#include "MetadataKind.def",点击进入,其中记录了所有类型的元数据,所以kind种类总结如下:
name | value |
---|---|
Class | 0x0 |
Struct | 0x200 |
Enum | 0x201 |
Optional | 0x202 |
ForeignClass | 0x203 |
Opaque | 0x300 |
Tuple | 0x301 |
Function | 0x302 |
Existential | 0x303 |
Metatype | 0x304 |
ObjCClassWrapper | 0x305 |
ExistentialMetatype | 0x306 |
HeapLocalVariable | 0x400 |
HeapGenericLocalVariable | 0x500 |
ErrorObject | 0x501 |
LastEnumerated | 0x7FF |
接着我们回到TargetMetaData
结构体定义中,找方法getClassObject
,在该方法中去匹配kind返回值是TargetClassMetadata
类型
如果是Class
,则直接对this
(当前指针,即metadata)强转
为ClassMetadata
。所以,TargetMetadata
和 TargetClassMetadata
本质上是一样的,因为在内存结构中,可以直接进行指针的转换,那么我们可以这么认为结构体其实就是TargetClassMetadata
。
接着,我们再看看TargetClassMetadata
里还有哪些成员
template
struct TargetClassMetadata : public TargetAnyClassMetadata {
...
//swift特有的标志
ClassFlags Flags;
//实力对象内存大小
uint32_t InstanceSize;
//实例对象内存对齐方式
uint16_t InstanceAlignMask;
//运行时保留字段
uint16_t Reserved;
//类的内存大小
uint32_t ClassSize;
//类的内存首地址
uint32_t ClassAddressPoint;
...
}
然后我们看看继承链 TargetClassMetadata
继承TargetAnyClassMetadata
继承TargetHeapMetadata
。
template
struct TargetAnyClassMetadata : public TargetHeapMetadata {
...
ConstTargetMetadataPointer Superclass;
TargetPointer CacheData[2];
StoredSize Data;
...
}
至此,当metadata的kind为Class时,有如下继承链:
- 类class在底层中的实际类型是
TargetClassMetadata
,而TargetMetaData
中只有一个
属性kind
,TargetAnyClassMetaData
中有4个
属性,分别是kind, superclass,cacheData、data
(图中未标出) - 当前Class在内存中所存放的属性由
TargetClassMetadata属性 + TargetAnyClassMetaData属性 + TargetMetaData属性
构成,所以得出的metadata的数据结构体如下所示
struct swift_class_t: NSObject{
void *kind;//相当于OC中的isa,kind的实际类型是unsigned long
void *superClass;
void *cacheData;
void *data;
uint32_t flags; //4字节
uint32_t instanceAddressOffset;//4字节
uint32_t instanceSize;//4字节
uint16_t instanceAlignMask;//2字节
uint16_t reserved;//2字节
uint32_t classSize;//4字节
uint32_t classAddressOffset;//4字节
void *description;
...
}
refCounts
接着我们看看refCounts
,refCounts
是InlineRefCounts
类型,搜索InlineRefCounts
typedef RefCounts InlineRefCounts;
InlineRefCounts是RefCounts
类型,搜索
RefCounts
是class类型,确切的说,refCounts是个指针,占8字节大小
。
综上所述
- swift类本质是
HeapObject
HeapObject
默认大小为16字节
: metadata(struct)8字节和refCounts(class)8字节- LGPerson的
age(Int)占8字节
,name(String)占16字节
,加上上面的,所以LGPerson
的size为40字节
总结
本篇文章首先大致讲述了Swift的编译流程,与OC最大的不同在于,swift在编译过程中会生成SIL中间代码
,通过对中间代码的分析,我明知道了Swift实例对象的初始化会调用__allocating_init()
方法,接着我们通过LGPerson样例分析了Swift类的底层结构HeapObject
,其默认包含了metadata(struct)
和refCounts(class)
,共16字节
大小。