Swift編譯簡介
首先需要了解的是,iOS開發(fā)的語言不管是OC還是Swift,后期都是通過LLVM進行編譯的,如下圖:

可看到:
OC通過clang編譯器將OC文件編譯成IR,然后再生成可執(zhí)行文件.o
Swift則是通過Swift編譯器編譯成IR,然后生成可執(zhí)行文件。
swift在編譯過程中使用的前段編譯器是swiftc,和我們之前在OC中使用的clang是有所區(qū)別的??梢酝ㄟ^如下命令來查看swiftc都能做什么樣的事情:
swiftc -h
如下圖

可以看出:
swift文件在被編譯成可執(zhí)行文件之前,會先被編譯成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 --> 內(nèi)存分配
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函數(shù):

-
%0,%1...在SIL中也叫寄存器,可以理解為日常開發(fā)中的常量,一旦賦值后就不可再修改,如果SIL中還要繼續(xù)使用,就需要不斷累加數(shù)字。這里說的寄存器是虛擬的,最終運行到機器上會使用真的寄存器。 -
alloc_global后面的參數(shù)s4main1tAA9YYTeacherCvp可以通過以下命令看出是什么:
xcrun swift-demangle s4main1tAA9YYTeacherCvp
其實就是經(jīng)過swift混寫后的字符串,如圖

可以看出:s4main1tAA9YYTeacherCvp實際就是YYTeacher里面的實例對象t.

這里也可以通過符號斷點和vscode源碼調(diào)試的方法來看一下Swift內(nèi)存分配過程中發(fā)生了什么?












在vscode中搜索_swift_allocObject,可以看出:


- 綜上可總結(jié)出
Swift內(nèi)存分配過程:
__allocating_init---->swift_allocObject---->_swift_allocObject_---->swift_showAlloc---->malloc - 實例對象
t本質(zhì)就是_swift_allocObject_的返回類型HeapObject -
Swift對象的內(nèi)存結(jié)構(gòu)HeapObject有兩個屬性:
struct HeapObject {
# 指針 默認(rèn)占8字節(jié)
HeapMetadata const *metadata;
# InlineRefCounts是 class RefCounts,所以RefCounts是一個對象,默認(rèn)占8字節(jié)
InlineRefCounts refCounts;
}
可看出HeapObject默認(rèn)占8 + 8 = 16字節(jié)
而OC中,實例對象本質(zhì)則是objc_object,里面有一個class_isa,默認(rèn)8字節(jié)。
通過SIL文件來分析Swift類結(jié)構(gòu)
通過在vscode中源碼分析可得如下圖關(guān)系所示:

可得出當(dāng)前metadata的數(shù)據(jù)結(jié)構(gòu):
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屬性
-
存儲屬性:
占用內(nèi)存空間的屬性
在上面例子class中,默認(rèn)聲明的屬性age和name就是存儲屬性,通過var(變量)或者let(常量)來修飾。
在SIL文件中也可以看到:

通過查看內(nèi)存地址也可以看出:

可見age和name都占用了內(nèi)存空間。
-
計算屬性:只有
get和set方法,不存儲值在內(nèi)存中
如下圖:area則為計算屬性,不占用內(nèi)存空間

在SIL中也可以看出area不占用內(nèi)存空間

計算屬性的本質(zhì):get和set方法,方法存放在metadata元數(shù)據(jù)中(OC中則存放在objc_class的Method_list里面)
-
屬性觀察者 :
willSet和didSet,作用是監(jiān)聽屬性的變化
class YYTeacher {
// 屬性觀察者
var name : String = "YY" {
// 新值存儲之前調(diào)用
willSet {
print("willSet newValue = \(newValue)")
}
// 新值存儲之后調(diào)用
didSet {
print("didSet oldValue = \(oldValue)")
}
}
}
var t = YYTeacher()
t.name = "newYY"
通過查看SIL文件中name的set方法:

可知:
- 在
willSet中可訪問到newValue和self - 在
didSet中可訪問到oldValue和self
注意:在init方法中調(diào)用屬性是不會觸發(fā)屬性觀察者的,以下面特殊情況為例。
class YYTeacher {
var age : Int = 20
var name : String = "YY" {
// 新值存儲之前調(diào)用
willSet {
print("willSet newValue = \(newValue)")
}
// 新值存儲之后調(diào)用
didSet {
print("didSet oldValue = \(oldValue)")
}
}
// 初始化當(dāng)前變量
init() {
// 不會觸發(fā)屬性觀察者
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自己都已經(jīng)實現(xiàn)了,想要通知外界完全可以在自己的get和set方法里面操作。
如果父類和子類中的同一屬性的屬性觀察者同時存在,那么調(diào)用順序是怎樣的?


注意:在子類的init方法中調(diào)用繼承的屬性會調(diào)用屬性觀察者,因為在調(diào)用之前先調(diào)用了super.init(),確保父類變量已經(jīng)初始化完成。
- 延遲存儲屬性
class YYTeacher {
lazy var age : Int = 12
}
- 用
lazy修飾的存儲屬性 - 延遲存儲屬性
必須有一個默認(rèn)的初始值,可選類型?和隱式可選類型!都不行 - 延遲存儲屬性在
第一次訪問的時候才會被賦值

被第一次訪問后,查看內(nèi)存:

- 延遲存儲屬性的本質(zhì):可選類型
Optional

從上圖可以看出:延遲存儲屬性本質(zhì)上是一個可選類型Optional,在沒有被訪問之前值為nil。get方法中通過switch枚舉值,跳轉(zhuǎn)分支來進行賦值操作。
- 延遲存儲屬性對類的
內(nèi)存大小的影響
如圖


通過上面了解到,延遲存儲屬性的本質(zhì)是一個可選類型,所以來研究一下可選類型的內(nèi)存大小。
通過控制臺打印得出:
MemoryLayout<Optional<Int>>.stride = 16---> 在內(nèi)存分配的過程中,為了讓它的地址是偶地址,字節(jié)對齊后,系統(tǒng)實際分配的內(nèi)存大小。(字節(jié)對齊:以空間換取時間,提高訪問效率)
MemoryLayout<Optional<Int>>.size = 9 --- > 從存儲開始到存儲結(jié)束占用的字節(jié)大小,即實際占用的內(nèi)存大小。
- 延遲存儲屬性并
不能保證線程安全
通過上面SIL中的get方法可以看到:如果有兩個線程同時訪問get方法,假如CPU在線程1剛執(zhí)行到bb2時就把時間片分給了線程2,線程2也剛剛執(zhí)行到bb2的時候又將時間片分給線程1,這時線程1執(zhí)行完bb2即賦值第一次,然后線程2執(zhí)行完bb2即賦值第二次,所以延遲存儲屬性并不能保證只被初始化一次。
- 類型屬性
- 使用關(guān)鍵字
static修飾 - 類型屬性必須有一個
默認(rèn)的初始值
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創(chuàng)建聲明一個實例對象
static let sharedInstance : YYTeacher = YYTeacher();
// 給當(dāng)前init添加訪問控制權(quán)限,不能再通過var t = YYTeacher()這種方式創(chuàng)建實例對象
private init(){}
}
// 只能通過這種方式獲取實例變量
var t = YYTeacher.sharedInstance
