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
