Swift编译简介
首先需要了解的是,iOS开发的语言不管是OC
还是Swift
,后期都是通过LLVM
进行编译的,如下图:
可看到:
OC
通过clang编译器
将OC文件编译成IR,然后再生成可执行文件.o
Swift
则是通过Swift编译器
编译成IR,然后生成可执行文件。
swift
在编译过程中使用的前段编译器是swiftc
,和我们之前在OC
中使用的clang
是有所区别的。可以通过如下命令来查看swiftc
都能做什么样的事情:
swiftc -h
如下图
可以看出:
swift
文件在被编译成可执行文件之前,会先被编译成SIL (Swift intermediate language)
文件。
分析SIL文件之前,先新建一个class:
class YYTeacher {
var age : Int = 20
var name : String = "YY"
}
var t = YYTeacher()
通过SIL文件来分析Swift对象
var t = YYTeacher()
这句代码类比OC
来说,实际做了两件事情:
alloc
--> 内存分配
init
--> 初始化操作
那么对于Swift
来说,做了什么事情呢?下面我们通过SIL
文件来观察一下。
- 打开
终端
进入项目所在目录,输入命令(二选一,建议使用第二条命令):
swiftc -emit-sil main.swift >> ./main.sil && open main.sil
# 用这个命令SIL文件更清晰
swiftc -emit-sil main.swift | xcrun swift-demangle >> ./main.sil && open main.sil
如果打开sil文件失败
,如图:
则自行到sil所在目录
手动选择使用vscode
打开,如图:
接下来看一下sil文件里面main函数:
-
%0
,%1
...在SIL
中也叫寄存器
,可以理解为日常开发中的常量
,一旦赋值后就不可再修改
,如果SIL中还要继续使用,就需要不断累加
数字。这里说的寄存器是虚拟
的,最终运行到机器上会使用真的寄存器。 -
alloc_global
后面的参数s4main1tAA9YYTeacherCvp
可以通过以下命令看出是什么:
xcrun swift-demangle s4main1tAA9YYTeacherCvp
其实就是经过swift
混写后的字符串,如图
可以看出:s4main1tAA9YYTeacherCvp
实际就是YYTeacher
里面的实例对象t
.
这里也可以通过符号断点
和vscode源码调试
的方法来看一下Swift内存分配
过程中发生了什么?
在vscode
中搜索_swift_allocObject
,可以看出:
- 综上可总结出
Swift
内存分配过程:
__allocating_init
---->swift_allocObject
---->_swift_allocObject_
---->swift_showAlloc
---->malloc
- 实例对象
t
本质就是_swift_allocObject_
的返回类型HeapObject
-
Swift
对象的内存结构HeapObject
有两个属性:
struct HeapObject {
# 指针 默认占8字节
HeapMetadata const *metadata;
# InlineRefCounts是 class RefCounts,所以RefCounts是一个对象,默认占8字节
InlineRefCounts refCounts;
}
可看出HeapObject
默认占8 + 8 = 16
字节
而OC
中,实例对象本质则是objc_object
,里面有一个class_isa
,默认8
字节。
通过SIL文件来分析Swift类结构
通过在vscode
中源码分析
可得如下图关系所示:
可得出当前metadata
的数据结构:
struct swift_class_t {
// 如果要与OC交互(继承NSObject),则kind则等同于void *isa;
void kind;
void *superClass
void *cacheData
void *data
uint32_t flags
uint32_t instanceAddressOffset
uint32_t instanceSize
uint16_t instanceAlignMask
uint16_t reserved
uint32_t classSize
uint32_t classAddressOffset
void *description
void * IVarDestroyer
// ...
};
Swift属性
-
存储属性:
占用内存
空间的属性
在上面例子class
中,默认声明的属性age
和name
就是存储属性
,通过var(变量)
或者let(常量)
来修饰。
在SIL
文件中也可以看到:
通过查看内存地址
也可以看出:
可见age
和name
都占用了内存空间。
-
计算属性:只有
get
和set
方法,不存储值在内存中
如下图:area
则为计算属性,不占用内存空间
在SIL
中也可以看出area不占用内存空间
计算属性的本质
:get
和set
方法,方法存放在metadata元数据
中(OC
中则存放在objc_class
的Method_list
里面)
-
属性观察者 :
willSet
和didSet
,作用是监听属性
的变化
class YYTeacher {
// 属性观察者
var name : String = "YY" {
// 新值存储之前调用
willSet {
print("willSet newValue = \(newValue)")
}
// 新值存储之后调用
didSet {
print("didSet oldValue = \(oldValue)")
}
}
}
var t = YYTeacher()
t.name = "newYY"
通过查看SIL
文件中name
的set
方法:
可知:
- 在
willSet
中可访问到newValue
和self
- 在
didSet
中可访问到oldValue
和self
注意
:在init
方法中调用属性
是不会触发属性观察者
的,以下面特殊情况为例。
class YYTeacher {
var age : Int = 20
var name : String = "YY" {
// 新值存储之前调用
willSet {
print("willSet newValue = \(newValue)")
}
// 新值存储之后调用
didSet {
print("didSet oldValue = \(oldValue)")
}
}
// 初始化当前变量
init() {
// 不会触发属性观察者
self.name = "newYY"
self.age = 18
}
}
var t = YYTeacher()
属性观察者可以定义在哪些地方呢?
- 定义的存储属性
- 继承的存储属性
class YYMathTeacher: YYTeacher {
override var age: Int {
willSet {
print("willSet newValue = \(newValue)")
}
didSet {
print("didSet oldValue = \(oldValue)")
}
}
}
- 继承的计算属性
YYTeacher
中计算属性age2
var age2 : Int {
get {
return age
}
set {
self.age = newValue
}
}
class YYMathTeacher: YYTeacher {
override var age2: Int {
willSet {
print("willSet newValue = \(newValue)")
}
didSet {
print("didSet oldValue = \(oldValue)")
}
}
}
注意:定义的计算属性里面不能添加属性观察者
,因为get和set自己都已经实现了,想要通知外界完全可以在自己的get和set方法里面操作。
如果父类和子类中的同一属性的属性观察者同时存在,那么调用顺序是怎样的?
注意:在子类的init方法中调用继承的属性会调用属性观察者,因为在调用之前先调用了super.init(),确保父类变量已经初始化完成
。
- 延迟存储属性
class YYTeacher {
lazy var age : Int = 12
}
- 用
lazy
修饰的存储属性
- 延迟存储属性
必须
有一个默认的初始值
,可选类型?
和隐式可选类型!
都不行 - 延迟存储属性在
第一次访问
的时候才会被赋值
被第一次访问后,查看内存:
- 延迟存储属性的本质:可选类型
Optional
从上图可以看出:延迟存储属性本质
上是一个可选类型Optional
,在没有被访问之前值为nil
。get
方法中通过switch
枚举值,跳转分支
来进行赋值
操作。
- 延迟存储属性对类的
内存大小
的影响
如图
通过上面了解到,延迟存储属性的本质是一个可选类型,所以来研究一下可选类型的内存大小
。
通过控制台打印得出:
MemoryLayout<Optional<Int>>.stride = 16
---> 在内存分配的过程中,为了让它的地址是偶地址
,字节对齐后,系统实际分配的内存大小
。(字节对齐
:以空间换取时间,提高访问效率
)
MemoryLayout<Optional<Int>>.size = 9
--- > 从存储开始到存储结束占用的字节大小,即实际占用的内存大小
。
- 延迟存储属性并
不能
保证线程安全
通过上面SIL
中的get
方法可以看到:如果有两个线程
同时访问get
方法,假如CPU
在线程1
刚执行到bb2
时就把时间片分给了线程2
,线程2
也刚刚执行到bb2
的时候又将时间片分给线程1
,这时线程1
执行完bb2
即赋值第一次
,然后线程2
执行完bb2
即赋值第二次
,所以延迟存储属性并不能
保证只被初始化一次
。
- 类型属性
- 使用关键字
static
修饰 - 类型属性必须有一个
默认
的初始值
class YYTeacher {
static var age : Int = 10
}
上面例子中age
是一个类型属性
,通过YYTeacher.age
来访问它。
- 类型属性只会被
初始化一次
通过SIL
可以看出通过static
修饰的属性是一个全局属性
:
通过上图可以看出:通过static修饰的类型属性可以保证该属性只被初始化一次。相比lazy
来说,static
声明的类型属性是:
-
全局
的 -
赋值
过程是一个线程安全
的过程
同时,可延伸出单例的正确写法:
OC
中单例写法
+ (instancetype)sharedInstance {
static Thread *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[Thread alloc] init];
});
return sharedInstance;
}
Swift2.0
以后的单例写法
class YYTeacher {
// 使用static let创建声明一个实例对象
static let sharedInstance : YYTeacher = YYTeacher();
// 给当前init添加访问控制权限,不能再通过var t = YYTeacher()这种方式创建实例对象
private init(){}
}
// 只能通过这种方式获取实例变量
var t = YYTeacher.sharedInstance