Swift编译过程
编译过程(OC、Swift的区别)
-
OC
中通过clang
编译器,编译成IR,然后再生成可执行文件.o(即机器码) -
swift
中通过swiftc
编译器,编译成IR,然后再生成可执行文件
iOS开发语言,不管是OC还是Swift,后端都是通过LLVM
进行编译的,如下图所示
Swift编译过程
下面是Swift中的编译流程,其中SIL
(Swift Intermediate Language),是Swift编译过程中的中间代码
,主要用于进一步分析和优化Swift代码。如下图所示,SIL
位于在AST
和LLVM
IR之间
我们可以通过swiftc -h
终端命令,查看swiftc的所有命令
例如:在main.swift文件定义如下代码
class YCTeacher {
var age: Int = 18
var name: String = "teacher"
}
var t = YCTeacher()
- 查看抽象语法树:
swiftc -dump-ast main.swift
- 生成
SIL
文件(Swift Intermediate Language):swiftc -emit-sil main.swift >> ./main.sil
,其中main的入口函数如下:
/ main
// `@main`:标识当前main.swift的`入口函数`,SIL中的标识符名称以`@`作为前缀
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
// `%0、%1` 在SIL中叫做寄存器,可以理解为开发中的常量,一旦赋值就不可修改,如果还想继续使用,就需要不断的累加数字(注意:这里的寄存器,与`register read`中的寄存器是有所区别的,这里是指`虚拟寄存器`,而`register read`中是`真寄存器`)
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
// `alloc_global`:创建一个`全局变量`,即代码中的`t`
alloc_global @$s4main1tAA9YCTeacherCvp // id: %2
// `global_addr`:获取全局变量地址,并赋值给寄存器%3
%3 = global_addr @$s4main1tAA9YCTeacherCvp : $*YCTeacher // user: %7
// `metatype`获取`YCTeacher`的`MetaData`赋值给%4
%4 = metatype $@thick YCTeacher.Type // user: %6
// 将`__allocating_init`的函数地址赋值给 %5
// function_ref YCTeacher.__allocating_init()
%5 = function_ref @$s4main9YCTeacherCACycfC : $@convention(method) (@thick YCTeacher.Type) -> @owned YCTeacher // user: %6
// `apply`调用 `__allocating_init` 初始化一个变量,赋值给%6
%6 = apply %5(%4) : $@convention(method) (@thick YCTeacher.Type) -> @owned YCTeacher // user: %7
// 将%6的值存储到%3,即全局变量的地址(这里与前面的%3形成一个闭环)
store %6 to %3 : $*YCTeacher // id: %7
// 构建`Int`,并`return`
%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'
- 从SIL文件中,可以看出,代码是经过混淆的,可以通过
xcrun swift-demangle
还原
xcrun swift-demangle s4main1tAA9YCTeacherCvp
$s4main1tAA9YCTeacherCvp ---> main.t : main.YCTeacher
-
SIL
更多语法信息,可参考github地址 - 在
SIL
文件中搜索s4main9YCTeacherC3age4nameACSi_SStcfC
,其内部主要是分配内存+初始化变量
// ************* main入口函数中的代码 ****************
// function_ref YCTeacher.__allocating_init(age:name:)
%13 = function_ref @$s4main9YCTeacherC3age4nameACSi_SStcfC : $@convention(method) (Int, @owned String, @thick YCTeacher.Type) -> @owned YCTeacher // user: %14
// s4main9YCTeacherC3age4nameACSi_SStcfC 实际上就是__allocating_init(age:name:)
// YCTeacher.__allocating_init(age:name:)
sil hidden [exact_self_class] @$s4main9YCTeacherC3age4nameACSi_SStcfC : $@convention(method) (Int, @owned String, @thick YCTeacher.Type) -> @owned YCTeacher {
// %0 "age" // user: %5
// %1 "name" // user: %5
// %2 "$metatype"
bb0(%0 : $Int, %1 : $String, %2 : $@thick YCTeacher.Type):
// 堆上分配内存空间
%3 = alloc_ref $YCTeacher // user: %5
// function_ref YCTeacher.init(age:name:) 初始化当前变量
%4 = function_ref @$s4main9YCTeacherC3age4nameACSi_SStcfc : $@convention(method) (Int, @owned String, @owned YCTeacher) -> @owned YCTeacher // user: %5
%5 = apply %4(%0, %1, %3) : $@convention(method) (Int, @owned String, @owned YCTeacher) -> @owned YCTeacher // user: %6
return %5 : $YCTeacher // id: %6
} // end sil function '$s4main9YCTeacherC3age4nameACSi_SStcfC'
对象的创建过程
符号断点调试
- 在工程中添加
__allocating_init
符号断点
- 发现其内部调用的是
swift_allocObject
源码调试
下面我们通过swift_allocObject
来探索swift中对象的创建过程
- 在
REPL
(Read Eval PrintLoop)swift交互式解释器中编写代码,也可以拷贝,并在HeapObject.cpp
文件中搜索swift_allocObject
函数加一个断点,然后定义一个实例对象t
- 其中
requiredSize
是分配的实际内存大小,40
-
requiredAlignmentMask
是swift中的字节对齐方式,这个和OC是一样的,必须是8
的倍数,不足的会自动补齐,目的是以空间换时间
,来提高内存操作效率
swift_allocObject源码分析
swift_allocObject
源码如下,主要有以下几个部分
- 通过
swift_slowAlloc
分配内存,并进行内存字节对齐 - 通过
new (object) HeapObject(metadata);
初始化一个实例对象 - 函数的返回值是
HeapObject
类型,所以当前对象的内存结构
就是HeapObject
的内存结构
static HeapObject *_swift_allocObject_(HeapMetadata const *metadata,
size_t requiredSize,
size_t requiredAlignmentMask) {
assert(isAlignmentMask(requiredAlignmentMask));
auto object = reinterpret_cast<HeapObject *>(
swift_slowAlloc(requiredSize, requiredAlignmentMask)); // 分配内存、字节对齐
// NOTE: this relies on the C++17 guaranteed semantics of no null-pointer
// check on the placement new allocator which we have observed on Windows,
// Linux, and macOS.
new (object) HeapObject(metadata); // 初始化一个实例对象
// If leak tracking is enabled, start tracking this object.
SWIFT_LEAKS_START_TRACKING_OBJECT(object);
SWIFT_RT_TRACK_INVOCATION(object, swift_allocObject);
return object;
}
- 在
Heap.cpp
文件中,进入swift_slowAlloc
函数,其内部主要是通过malloc
在堆
中分配size大小的内存空间
,并返回内存地址p
,主要是用来存储实例变量
void *swift::swift_slowAlloc(size_t size, size_t alignMask) {
void *p;
// This check also forces "default" alignment to use AlignedAlloc.
if (alignMask <= MALLOC_ALIGN_MASK) {
#if defined(__APPLE__)
p = malloc_zone_malloc(DEFAULT_ZONE(), size);
#else
p = malloc(size); // 堆中创建size大小的内存空间,用于存储实例变量
#endif
} else {
size_t alignment = (alignMask == ~(size_t(0)))
? _swift_MinAllocationAlignment
: alignMask + 1;
p = AlignedAlloc(size, alignment);
}
if (!p) swift::crash("Could not allocate memory.");
return p;
}
- 进入
HeapObject
初始化方法,需要两个参数metadata
、refCounts
struct HeapObject {
/// This is always a valid pointer to a metadata object.
HeapMetadata const *metadata;
SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;
#ifndef __swift__
HeapObject() = default;
// Initialize a HeapObject header as appropriate for a newly-allocated object.
constexpr HeapObject(HeapMetadata const *newMetadata)
: metadata(newMetadata)
, refCounts(InlineRefCounts::Initialized)
{ }
- 其中
metadata
类型是HeapMetadata
,是一个指针类型,占8
字节大小 -
refCounts
(引用计数,类型是InlineRefCounts
,而InlineRefCounts
是一个类RefCounts
的别名,占8个字节),swift采用arc引用计数
总结
- 对于实例对象
t
来说,其本质是一个HeapObject
结构体,默认16
字节内存大小(metadata
8字节 +refCounts
8字节),与OC的对比如下- OC中实例对象的本质是
结构体
,是以objc_object
为模板继承的,其中有一个isa
指针,占8字节 - Swift中实例对象,默认的比OC中多了一个
refCounted
引用计数大小,默认属性占16
字节
- OC中实例对象的本质是
- Swift中对象的内存分配流程是:
__allocating_init --> swift_allocObject_ --> _swift_allocObject --> swift_slowAlloc --> malloc
-
init
在其中的职责就是初始化变量,这点与OC中是一致的
针对上面的分析,我们还遗留了两个问题:metadata
是什么,40
是怎么计算的?下面来继续探索
在demo中,我们可以通过Runtime
方法获取类的内存大小
这点与在源码调试时左边local的requiredSize
值是相等的,从HeapObject
的分析中我们知道了,一个类在没有任何属性的情况下,默认占用16
字节大小
对于Int
、String
类型,进入其底层定义,两个都是结构体类型,那么是否都是8字节呢?可以通过打印其内存大小来验证
//********* Int底层定义 *********
@frozen public struct Int : FixedWidthInteger, SignedInteger {...}
//********* String底层定义 *********
@frozen public struct String {...}
//********* 验证 *********
print(MemoryLayout<Int>.stride)
print(MemoryLayout<String>.stride)
//********* 打印结果 *********
8
16
- 从打印的结果中可以看出,
Int
类型占8
字节,String
类型占16
字节(后面文章会进行详细讲解),这点与OC中是有所区别的
所以这也解释了为什么YCTeacher
的内存大小等于40
,即40 = metadata(8字节) +refCount(8字节)+ Int(8字节)+ String(16字节)
探索Swift中类的结构
- 在OC中类是从
objc_class
模板继承过来的 - 在Swift中,类的结构在底层是
HeapObject
,其中有 metadata + refCounts
HeapMetadata类型分析
- 进入
HeapMetadata
定义,是TargetHeapMetaData
类型的别名,接收了一个参数Inprocess
using HeapMetadata = TargetHeapMetaData<Inprocess>;
- 进入
TargetHeapMetaData
定义,其本质是一个模板类型
,其中定义了一些所需的数据结构。这个结构体中没有属性,只有初始化
方法,传入了一个MetadataKind
类型的参数(该结构体没有,那么只有在父类中了)这里的kind
就是传入的Inprocess
//模板类型
template <typename Runtime>
struct TargetHeapMetadata : TargetMetadata<Runtime> {
using HeaderType = TargetHeapMetadataHeader<Runtime>;
TargetHeapMetadata() = default;
//初始化方法
constexpr TargetHeapMetadata(MetadataKind kind)
: TargetMetadata<Runtime>(kind) {}
#if SWIFT_OBJC_INTEROP
constexpr TargetHeapMetadata(TargetAnyClassMetadata<Runtime> *isa)
: TargetMetadata<Runtime>(isa) {}
#endif
};
- 进入
TargetMetaData
定义,有一个kind
属性,kind
的类型就是之前传入的Inprocess
。从这里可以得出,对于kind
,其类型就是unsigned long
,主要用于区分是哪种类型的元数据
//******** TargetMetaData 定义 ********
struct TargetMetaData{
using StoredPointer = typename Runtime: StoredPointer;
...
StoredPointer kind;
}
//******** Inprocess 定义 ********
struct Inprocess{
...
using StoredPointer = uintptr_t;
...
}
//******** uintptr_t 定义 ********
typedef unsigned long uintptr_t;
从TargetHeapMetadata
、TargetMetaData
定义中,均可以看出初始化方法中参数kind
的类型是MetadataKind
- 进入
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
const TargetClassMetadata<Runtime> *getClassObject() const;
//******** 具体实现 ********
template<> inline const ClassMetadata *
Metadata::getClassObject() const {
//匹配kind
switch (getKind()) {
//如果kind是class
case MetadataKind::Class: {
// Native Swift class metadata is also the class object.
//将当前指针强转为ClassMetadata类型
return static_cast<const ClassMetadata *>(this);
}
case MetadataKind::ObjCClassWrapper: {
// Objective-C class objects are referenced by their Swift metadata wrapper.
auto wrapper = static_cast<const ObjCClassWrapperMetadata *>(this);
return wrapper->Class;
}
// Other kinds of types don't have class objects.
default:
return nullptr;
}
}
这一点,我们可以通过lldb
来验证
-
po metadata->getKind()
,得到其kind是Class -
po metadata->getClassObject()
、x/8g 0x0000000110efdc70,这个地址中存储的是元数据信息!
所以,TargetMetadata
和 TargetClassMetadata
本质上是一样的,因为在内存结构中,可以直接进行指针的转换
,所以可以说,我们认为的结构体
,其实就是TargetClassMetadata
- 进入
TargetClassMetadata
定义,继承自TargetAnyClassMetadata
,有以下这些属性,这也是类结构的部分
template <typename Runtime>
struct TargetClassMetadata : public TargetAnyClassMetadata<Runtime> {
...
//swift特有的标志
ClassFlags Flags;
//实例对象内存大小
uint32_t InstanceSize;
//实例对象内存对齐方式
uint16_t InstanceAlignMask;
//运行时保留字段
uint16_t Reserved;
//类的内存大小
uint32_t ClassSize;
//类的内存首地址
uint32_t ClassAddressPoint;
...
}
- 进入
TargetAnyClassMetadata
定义,继承自TargetHeapMetadata
template <typename Runtime>
struct TargetAnyClassMetadata : public TargetHeapMetadata<Runtime> {
...
ConstTargetMetadataPointer<Runtime, swift::TargetClassMetadata> Superclass;
TargetPointer<Runtime, void> CacheData[2];
StoredSize Data;
...
}
总结
综上所述,当metadata
的kind
为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;
...
}
与OC的区别
-
实例对象 & 类
- OC中的
实例对象本质
是结构体
,是通过底层的objc_object
模板创建,类是继承自objc_class
- Swift中的
实例对象本质
也是结构体
,类型是HeapObject
,比OC多了一个refCounts
- OC中的
-
方法列表
- OC中的方法存储在
objc_class
结构体class_rw_t
的methodList
中 - swift中的方法存储在
metadata
元数据中
- OC中的方法存储在
-
引用计数
- OC中的ARC维护的是
散列表
- Swift中的ARC是对象内部有一个
refCounts
属性
- OC中的ARC维护的是
Swift属性
在swift中,属性主要分为以下几种
- 存储属性
- 计算属性
- 延迟存储属性
- 类型属性
存储属性
存储属性分为两种:
- 常量存储属性,用
let
修饰 - 变量存储属性,用
var
修饰
存储属性特征:会占用分配实例对象的内存空间
计算属性
计算属性是不占用内存空间
的,本质是set/get
方法
属性观察者(didSet、willSet)
-
willSet
:新值存储之前调用newValue
-
didSet
:新值存储之后调用oldValue
问题1:init方法中是否会触发属性观察者?
以下代码中,init方法中设置name,是否会触发属性观察者?
class YCTeacher{
var name: String = "测试"{
//新值存储之前调用
willSet{
print("willSet newValue \(newValue)")
}
//新值存储之后调用
didSet{
print("didSet oldValue \(oldValue)")
}
}
init() {
self.name = "teacher"
}
}
运行结果发现,并没有走willSet、didSet中的打印方法,所以有以下结论:
- 在
init
方法中,如果调用属性,是不会触发
属性观察者的 - init中主要是
初始化当前变量
,除了默认的前16个字节,其他属性会调用memset
清理内存空间(因为有可能是脏数据,即被别人用过),然后才会赋值
问题2:哪里可以添加属性观察者?
主要有以下三个地方可以添加:
- 1、类中定义的存储属性
- 2、通过类继承的存储属性
- 3、通过类继承的计算属性
问题3:子类和父类的属性同时存在didset、willset时,其调用顺序是什么?
class YCTeacher{
var age: Int = 18{
//新值存储之前调用
willSet{
print("父类 willSet newValue \(newValue)")
}
//新值存储之后调用
didSet{
print("父类 didSet oldValue \(oldValue)")
}
}
var age2: Int {
get{
return age
}
set{
self.age = newValue
}
}
}
class YCMediumTeacher: YCTeacher{
override var age: Int{
//新值存储之前调用
willSet{
print("子类 newValue \(newValue)")
}
//新值存储之后调用
didSet{
print("子类 didSet oldValue \(oldValue)")
}
}
}
var t = YCMediumTeacher()
t.age = 20
运行结果如下:
结论:对于同一个属性,子类和父类都有属性观察者,其顺序是:先子类willset,后父类willset,再父类didset, 子类的didset,即:子父 父子
延迟存储属性
- 使用
lazy
修饰的存储属性 - 延迟属性必须有一个默认的初始值
- 延迟存储在第一次访问的时候才被赋值
- 延迟存储属性并不能保证线程安全
- 延迟存储属性对实例对象大小的影响
类型属性
- 使用关键字
static
修饰,且是一个全局变量
- 类型属性必须有一个
默认的初始值
- 类型属性只会被
初始化一次
,线程安全
单例的写法
//****** Swift单例 ******
class YCTeacher{
//1、使用 static + let 创建声明一个实例对象
static let shareInstance = YCTeacher.init()
//2、给当前init添加private访问权限
private init(){ }
}
//使用(只能通过单例,不能通过init)
var t = YCTeacher.shareInstance
//****** OC单例 ******
@implementation YCTeacher
+ (instancetype)shareInstance{
static YCTeacher *shareInstance = nil;
dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shareInstance = [[YCTeacher alloc] init];
});
return shareInstance;
}
@end
总结
-
存储属性
会占用实例变量的内存空间 -
计算属性
不会占用内存空间,其本质是set/get
方法 - 属性观察者
-
willset
:新值存储之前调用,先通知子类,再通知父类(因为父类中可能需要做一些额外的操作),即子父
-
didSet
:新值存储完成后,先告诉父类,再通知子类(父类的操作优先于子类),即父子
- 类中的
init
方法赋值不会触发
属性观察 - 属性可以添加在
类定义的存储属性、继承的存储属性、继承的计算属性
中 - 子类调用父类的
init
方法,会触发
观察属性
-
- 延迟存储属性
- 使用
lazy
修饰存储属性,且必须有一个默认值
- 只有在
第一次被访问时才会被赋值
,且是线程不安全
的 - 使用lazy和不使用lazy,会
对实例对象的内存大小有影响
,主要是因为lazy在底层是optional
类型,optional的本质是enum
,除了存储属性本身的内存大小,还需要一个字节用于存储case
- 使用
- 类型属性
- 使用
static
修饰,且必须有一个默认初始值
- 是一个全局变量,只会被
初始化一次
,是线程安全
的 - 用于创建
单例
对象:- 使用
static + let
创建实例变量 -
init
方法的访问权限为private
- 使用
- 使用