iOS-Swift 獨孤九劍:八、協議的本質

語言: CN / TW / HK

highlight: a11y-dark

一、協議的基本語法

1. 協議的定義

協議可以用來定義方法、屬性、下標的宣告,協議可以被列舉、結構體、類遵守(多個協議之間用逗號隔開)。 ```swift // 協議定義方法、屬性、下標 protocol Drawable { func draw() var x: Int { get set } var y: Int { get } subscript(index: Int) -> Int { get} }

protocol Protocol1 {} protocol Protocol2 {} protocol Protocol3 {} // 遵守協議 class Person: Protocol1, Protocol2, Protocol3 {} ```

協議中定義方法時不能有預設引數值,且預設情況下,協議中定義的內容必須全部都實現。如果我們不想強制讓遵循協議的型別實現,可以使用 optional 作為字首放在協議的定義,並且 protocol 和 optional 前要加上 @objc。 swift @objc protocol Incrementable { @objc optional func increment(by: Int) }

2. 協議中的屬性

  • 協議中定義屬性時必須用var關鍵字。

  • 實現協議時的屬性許可權要不小於協議中定義的屬性許可權。協議定義 get、set,用 var 儲存屬性或 get、set 計算屬性去實現,協議定義 get,用任何屬性都可以實現。

例如: ```swift protocol Drawable { func draw() var x: Int { get set } var y: Int { get } subscript(index: Int) -> Int { get} }

class Person: Drawable { func draw() { print("Person draw") }

var x: Int = 0
var y: Int = 0
subscript(index: Int) -> Int {
    set{}
    get{ index }
}

} ```

3. 協議中的 static、class、mutating 和 init

  • static 為了保證通用,協議中必須用 static 定義型別方法、型別屬性、型別下標。 ```swift protocol Drawable { static func draw() }

class Person: Drawable { // class func draw static func draw() { print("Person draw") } }

Person.draw() // Person draw ```

  • mutating 只有將協議中的例項方法標記為 mutating,才允許結構體、列舉的具體實現修改自身記憶體。類在實現方法時不用加 mutating,列舉、結構體才需要加 mutating。 ```swift protocol Drawable { mutating func draw() }

class Person: Drawable { func draw() { print("Person draw") } }

struct Point: Drawable { mutating func draw() { print("Point draw") } } - init 協議中還可以定義初始化器 init,非 final 類實現時必須加上 required。swift final class Size: Drawable { init(x: Int, y: Int) { } }

class Point: Drawable { required init(x: Int, y: Int) { } } ```

如果從協議實現的初始化器,剛好是重寫了父類的指定初始化器,那麼這個初始化必須同時加required、override。 ```swift protocol Livable { init(name: String) }

class Person { init(name: String) {} }

class Student: Person, Livable { required override init(name: String) { super.init(name: name) } } ```

協議中定義的 init?、init!,可以用 init、init?、init! 去實現,協議中定義的 init,可以用 init、init! 去實現。 ```swift protocol Livable { init() init?(age:Int) init!(no:Int) }

class Person: Livable { required init() {} // required init!() {}

required init?(age: Int) {}
//    required init(age: Int) {}
//    required init!(age: Int) {}

required init!(no: Int) {}
//    required init(no: Int) {}
//    required init?(no: Int) {}

} ```

4. 協議中的繼承和組合

一個協議可以繼承其他協議。例如: ```swift protocol Runnable { func run() }

protocol Livable: Runnable { func breath() }

class Person: Livable { func breath() {} func run() {} } ```

協議組合,可以包含1個類型別(最多1個)。我們來看下面的例子: ```swift // 接收 Person 或者其子類的例項 func fn0(obj: Person) {}

// 接收遵守 Livable 協議的例項 func fn1(obj: Livable) {}

// 接收同時遵守 Livable、Runnable 協議的例項 func fn2(obj: Livable & Runnable) {}

// 接收同時遵守 Livable、Runnable 協議、並且是 Person 或者其子類的例項 func fn3(obj: Person & Livable & Runnable) {} ```

fn3 中,引數 obj 協議組合的宣告太長了,我們可以用 typealias 給協議組合取別名,例如: swift typealias RealPerson = Person & Livable & Runnable // 接收同時遵守 Livable、Runnable 協議、並且是 Person 或者其子類的例項 func fn3(obj: RealPerson) {}

5. CaseIterable 和 CustomStringConvertible

  • 讓列舉遵守 CaseIterable 協議,可以實現遍歷列舉值。 ```swift enum Season: CaseIterable { case spring, summer, autumn, winter }

let seasons = Season.allCases print(seasons.count) // 4

for season in seasons { print(season) } // spring summer autumn winter ```

  • 遵守 CustomStringConvertible、CustomDebugStringConvertible 協議,都可以自定義例項的列印字串。 ```swift class Person: CustomStringConvertible, CustomDebugStringConvertible { var age = 0 var description: String{ "person_(age)" } var debugDescription: String{ "debug_person_(age)" } }

var person = Person() print(person) // person_0 debugPrint(person) // debug_person_0 ```

debugPrint po 列印.png

  • print 呼叫的是 CustomStringConvertible 協議的 description。

  • debugPrint、po 呼叫的是 CustomDebugStringConvertible 協議的 debugDescription。

6. 類專用協議

在協議後面寫上 :AnyObject 代表只有類能遵守這個協議,在協議後面寫上 :class 也代表只有類能遵守這個協議。 swift protocol MyProtocol: AnyObject {} swift protocol MyProtocol: class {}

二、witness_table

witness_table 翻譯過來叫做見證表,它是用來幹什麼,我們接下來對它進行一個初步的認識。

《方法》這篇文章中我們知道,類的方法的排程是通過虛擬函式表(VTable)查詢到對應的函式進行呼叫的,而結構體的方法直接就是拿到函式的地址進行呼叫。那麼協議中宣告的方法呢,如果類或者結構體遵守這個協議,然後實現協議方法,它是如何去查詢函式的地址進行呼叫的呢。

1. witness_table 的引入

我們先宣告一份協議 Born,裡面有一個 born(:) 方法。類 - Person 遵守 Born 並實現 born(:) 方法,程式碼如下: ```swift protocol Born { func born(_ nickname: String) }

class Person: Born { var nickname: String? func born(_ nickname: String) { self.nickname = nickname } }

let p = Person() p.born("Coder_張三") ```

接下來我們把當前的 main.swift 檔案編譯成 main.sil 檔案,通過 sil 程式碼來觀察是否有 VTable。編譯完成後找到 main 函式,檢視 born(:) 方法的呼叫,如圖:

sil main 函式.png

注意看,born(:) 的型別在 sil 中是 class_method 型別的,在 SIL參考文件 中有介紹,class_method 型別的方法是通過 VTable 查詢的,如圖:

class_method 的介紹.png

接下來我們看到 main.sil 檔案最底部的程式碼,如圖:

sil_vtable 和 sil_witness_table.png

可以看到,born(:) 確實是儲存在 VTable 當中了,但是下面的 witness_table 是用來幹啥的,並且裡面也有一個 born(:),這是個啥。接下來我幹一件事,我把變數 p 宣告為 Born 協議,程式碼如下: swift let p: Born = Person()

接下來重新將 main.swift 檔案編譯成 main.sil 檔案,然後直接看 main 函式,如圖:

witness_method 型別.png

此時,我們發現函式的型別變了,變成了 witness_method 型別的,我們來看 SIL參考文件 中是如何介紹 witness_method 的:

witness_method 文件介紹.png

翻譯如下:

查詢受該協議約束的泛型型別變數的協議方法的實現。結果將在原始協議的 Self 原型上是通用的,並具有 witness_method 呼叫約定。如果引用的協議是 @objc 協議,則結果型別具有 objc 呼叫約定。

啥意思呢,我們全域性搜尋 @protocol witness for main.Born.born(Swift.String) -> (),找到它的實現,如圖:

witness born 的實現.png

注意看,它最終還是會去查詢遵守它的類中的 VTable 進行方法的排程。我們兩次的測試唯一的區別在於是否指定變數的型別為 Born 的協議型別,也可以理解為這個呼叫的方式和我這個變數指定的靜態型別有關。

總結如下: - 如果例項物件的靜態型別就是確定的型別,那麼這個協議方法通過 VTalbel 進行排程。

  • 如果例項物件的靜態型別是協議型別,那麼這個協議方法通過 witness_table 中對應的協議方法,然後通過協議方法去查詢遵守協議的類的 VTable 進行排程。

2. 結構體的 witness_table

知道類的 witness_table 排程情況了之後,我們來看一下結構體的 witness_table,還是老辦法,通過 sil 程式碼分析,程式碼如下: ```swift protocol Born { func born(_ nickname: String) }

struct Person: Born { var nickname: String? func born(_ nickname: String) { self.nickname = nickname } }

let p: Born = Person() p.born("Coder_張三") ```

接下來重新將 main.swift 檔案編譯成 main.sil 檔案,然後直接看 main 函式,如圖:

結構體協議方法的呼叫.png

我們再來看一下彙編程式碼,如圖:

結構體協議彙編的呼叫.png

可以看到,結構體呼叫協議方法的方式直接就是函式地址呼叫。當我指定這個變數的型別為 Born 協議的時候,sil main 函式的實現如下:

結構體指定變數為協議型別的實現.png

注意看,這個時候它的型別變成了 witness_method ,我們再來看這個方法對應的 witness_method 的實現,如圖:

結構體 witness_method 的實現.png

可以看到,它最終還是找到了結構體 born(:) 方法的地址直接進行呼叫。那這個就是結構體 witness_method 的呼叫情況。

3. 在協議的 extention 提供協議方法的預設實現

如果對一個協議進行一個 extension,並且實現協議的方法。同時,遵守這個協議的類也實現這個協議方法。那麼,通過這個類呼叫協議方法的時候,呼叫的是類中實現的協議方法。

程式碼如下: ```swift protocol Born { func born(_ nickname: String) }

extension Born { func born(_ nickname: String) { print("Born born(:)") } }

class Person: Born { func born(_ nickname: String) { print("Person born(:)") } }

let p = Person() p.born("Coder_張三") // Person born(:) ```

如果在協議中沒有宣告這個協議方法,但是在協議的 extension 實現了,遵守這個協議的類也實現了這個方法。那麼,通過這個類呼叫這個協議方法的時候,呼叫的還是類中實現的方法,但是如果指定了這個變數的型別是協議型別,呼叫的就是協議的 extension 中實現的方法。

程式碼如下: ```swift protocol Born {}

extension Born { func born(_ nickname: String) { print("Born born(:)") } }

class Person: Born { func born(_ nickname: String) { print("Person born(:)") } }

let p: Born = Person() p.born("Coder_張三") // Born born(:) ```

那其實對於第一種情況來講,這個協議方法的呼叫流程是和第 1 點中驗證的流程結果是一樣的,想驗證的靚仔可以自己編譯成 sil 程式碼去驗證對比。我們接下來主要看第二種情況,我們直接看 main 函式和 sil_witness_table,如圖:

協議的擴充套件中實現協議方法.png

可以看到,針對於第二種情況,它直接就是拿到 extension 中的函式地址進行呼叫,並且 sil_witness_table 中沒有任何方法。

需要注意的是,這個時候我們指定了 p 變數的型別為協議型別,但其實就算指定變數 p 的型別為 Person,sil_witness_table 中還是沒有任何方法,這個感興趣的靚仔可以去嘗試,這裡就不一一貼圖了,比較麻煩。

那這裡我們來做一個總結: - 首先 sil_witness_table 有沒有方法取決於在協議中有沒有宣告協議方法。

  • 如果 sil_witness_table 中沒有方法,那麼遵守這份協議的型別該 VTable 排程就 VTable 排程,該直接函式地址呼叫就直接函式地址呼叫。

  • 如果 sil_witness_table 中有方法,那麼是否通過 witness_method 去呼叫取決於當前例項的靜態型別是否是協議型別。如果不是,該怎麼排程就怎麼排程。如果是,那麼就通過 witness_method 進行方法的排程。

總的來說當 sil_witness_table 中有方法並且通過 witness_method 呼叫的時候,無非就是多了一層函式呼叫。

4. sil_witness_table 在繼承關係的情況

  • 當一份協議被多個類遵守的時候,那麼在各自類中都會有一個 sil_witness_table。

  • 當一個類遵守多份協議的時候,那麼在這個類中,都有一個每份協議對應的 sil_witness_table,也就是會有很多個 sil_witness_table,這個取決於協議的數量。

  • 如果一個類遵守了一份協議,這個類必然會有一個 sil_witness_table,那麼這個類的子類和父類是共用一份 sil_witness_table 的。

以上這三點都是可以通過 sil 的程式碼進行驗證對比的,感興趣的靚仔可以自己試著驗證。這裡就不貼圖了,比較麻煩。

三、witness_table 記憶體佈局和記憶體結構

1. witness_table 在記憶體中的位置

我們接下來看一段比較有意思的程式碼,如下: ```swift protocol Shape { var area: Double { get } }

class Circle: Shape { var radius: Double

init(_ radius: Double) {
    self.radius = radius
}

var area: Double {
    get {
        return radius * radius * 3.14
    }
}

}

print("Circle size: (MemoryLayout.size)") // Circle size: 8 print("Shape size: (MemoryLayout.size)") // Shape size: 40 ```

我們通過 MemoryLayout 獲取型別的 Size 的時候,發現協議型別和類型別的 size 不一致,類型別的 size 等於 8 這是正常的,因為類的記憶體在堆空間,這個 8 僅僅只是一個指標型別的大小,要想拿到類真正的大小得通過 class_getInstanceSize 函式。

這個協議型別的 size 等於 40 又是怎麼回事呢,我們接下來在測試一段程式碼,如下: ```swift let c1: Circle = Circle(10) let c2: Shape = Circle(20)

print("c1 size: (MemoryLayout.size(ofValue: c1))") // c1 size: 8 print("c2 size: (MemoryLayout.size(ofValue: c2))") // c2 size: 40 ```

我們發現,同樣是 Circle 的例項,但是當例項指定為協議型別的時候,這個例項的 size 就變成了 40。這個時候,代表著 c1 和 c2 的記憶體結構不一致了。

那對於 c1 變數的記憶體地址我們應該知道,c1 儲存的是它堆空間例項物件的地址,我們來看一下它的記憶體佈局,如圖:

c1 的記憶體佈局.png

這個就是 c1 的記憶體佈局,並且我們通過 expr -f float -- <記憶體地址> 表示式打印出了 radius 的值。

我們接下來看 c2 的記憶體佈局,如圖:

c2 的記憶體佈局.png

注意看: - 第一個 8 位元組的記憶體儲存的依然是堆空間的地址值。

  • 第二個和第三個 8 位元組儲存的是啥我們也不知道是什麼。

  • 第四個 8 位元組儲存的是堆空間 metadata 的地址。

  • 最後的 8 位元組儲存的其實是 witness_table 的地址。

那怎麼知道最後的 8 位元組儲存的就是 witness_table 的地址呢?最後的 8 位元組記憶體地址為 0x0000000100004028,我們接下來開啟彙編除錯,找到 c2 的建立後找到 witness_table 相關的程式碼,如圖:

彙編驗證 witness_table 儲存的位置.png

如圖所示,所以最後的 8 位元組儲存的其實是 witness_table 的地址。通過以上的分析,就可以得出 c2 這個型別變數的大致結構,程式碼如下: swift struct ProtoclInstaceStruct { var heapObject: UnsafeRawPointer var unkown1: UnsafeRawPointer var unkown2: UnsafeRawPointer var metadata: UnsafeRawPointer var witness_table: UnsafeRawPointer }

2. witness_table 的記憶體結構

通過第 1 點我們已經知道了 witness_table 在記憶體中儲存的位置,那這個 witness_table 的記憶體結構是怎麼樣的呢,這個時候就可以通過 IR 程式碼去進行分析了。IR 的語法和如何編譯成 IR 程式碼在《閉包及其本質分析》《方法》這兩篇文章中有介紹。

接下來我們就直接將當前的 main.swift 檔案編譯成 main.ll 檔案,程式碼還是第一點的程式碼,只不過為了避免干擾我把 c1 變數和 print 列印註釋了。編譯成 main.ll 檔案後我們直接看 main 函式,程式碼如下: ```swift define i32 @main(i32 %0, i8 %1) #0 { entry: %2 = bitcast i8 %1 to i8 // 獲取 Circle 的 metadata %3 = call swiftcc %swift.metadata_response @"type metadata accessor for main.Circle"(i64 0) #7 %4 = extractvalue %swift.metadata_response %3, 0 // %swift.type = type { i64 } // %swift.refcounted = type { %swift.type, i64 } // %T4main6CircleC = type <{ %swift.refcounted, %TSd }> // 建立 Circle 的例項,此時這個例項的結構為:{ %swift.refcounted, %TSd } %5 = call swiftcc %T4main6CircleC @"main.Circle.__allocating_init(Swift.Double) -> main.Circle"(double 2.000000e+01, %swift.type swiftself %4)

// %T4main5ShapeP = type { [24 x i8], %swift.type*, i8** },%T4main5ShapeP 本質上是一個結構體
// 注意看,getelementptr為獲取結構體成員,i32 0 結構體的記憶體地址,拿到這個結構體後將 %4 儲存到這個結構體的第二個成員變數上
// 也就是將 metadata 儲存到這個結構體的第二個成員變數上,此時這個結構體的結構為:{ [24 x i8], metadata, i8** }
store %swift.type* %4, %swift.type** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"main.c2 : main.Shape", i32 0, i32 1), align 8

// 這一行在獲取 witness table,然後將 witness table 儲存到 %T4main5ShapeP 這個結構體的第三個成員變數上(因為取的是 i32 2)
// 此時 %T4main5ShapeP 的結構為:{ [24 x i8], metadata, witness_table }
store i8** getelementptr inbounds ([2 x i8*], [2 x i8*]* @"protocol witness table for main.Circle : main.Shape in main", i32 0, i32 0), i8*** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"main.c2 : main.Shape", i32 0, i32 2), align 8

// [24 x i8] 是 24 個 Int8 陣列,記憶體中等價 [3 x i64] 陣列,等價於 %T4main5ShapeP = type { [3 x i64], %swift.type*, i8** }
// 這裡是將 %T4main5ShapeP 這個結構體強制轉換成 %T4main6CircleC,此時的結構為:{ [3 x i64], metadata, witness_table }
// 然後把 %5 存放到 %T4main5ShapeP 的第一個元素。所以最後的結構為:{ [%T4main6CircleC*, i64, i64], metadata, witness_table },
store %T4main6CircleC* %5, %T4main6CircleC** bitcast (%T4main5ShapeP* @"main.c2 : main.Shape" to %T4main6CircleC**), align 8
ret i32 0

} ```

通過這一段程式碼的解讀也進而驗證了第 1 點推斷出來的 c2 的記憶體結構。接下來我們還需要知道 witness_table 的記憶體結構,在 IR 中 witness_table 的結構如下:

witness_table 在 IR 中的結構.png

可以看到,這個 witness_table 的結構中有兩個成員,那麼根據這個資訊,還原出來的 witness_table 的結構如下: swift struct TargetWitnessTable{ var protocol_conformance_descriptor: UnsafeRawPointer var protocol_witness: UnsafeRawPointer }

那麼此時,ProtoclInstaceStruct 的結構就變成如下程式碼: swift struct ProtoclInstaceStruct { var heapObj: UnsafeRawPointer var unkown1: UnsafeRawPointer var unkown2: UnsafeRawPointer var metadata: UnsafeRawPointer var witness_table: UnsafeMutablePointer<TargetWitnessTable> }

3. 原始碼分析 witness_table 的記憶體結構

接下來我們通過原始碼來分析 witness_table 的記憶體結構,我們全域性搜尋 TargetWitnessTable,在 Metadata.h 檔案中找到 TargetWitnessTable,如圖:

原始碼的 TargetWitnessTable.png

注意看,原始碼中的註釋也清楚的寫著這個是一個協議的見證表,並且,此時我們知道第 2 點分析出來的 protocol_conformance_descriptor 是一個 TargetProtocolConformanceDescriptor,找到這個結構的定義,發現它有以下成員,如圖:

TargetProtocolConformanceDescriptor 的成員.png

我們看 Protocol 這個成員變數,它是一個相對型別指標,其型別的結構為 TargetProtocolDescriptor,相對型別指標在《元型別以及 Mirror 原始碼和 HandyJson 分析還原列舉、結構體、類的 Metadata》這篇文章中有介紹,而且我們已經把這個相對型別指標給還原了出來,我們用的時候直接複製過來就好了。

現在需要還原 TargetProtocolDescriptor 的結構,TargetProtocolDescriptor 是繼承自 TargetContextDescriptor 的,TargetContextDescriptor 我們應該無比的熟悉了,在上面提到的文章中也有介紹。所以,TargetProtocolDescriptor 必然有 Flags 和 Parent 兩個成員變數,我們再看一下它自身有什麼,如圖:

TargetProtocolDescriptor 的結構.png

此時此刻,TargetProtocolDescriptor 的結構可以還原出來了,程式碼如下: swift struct TargetProtocolDescriptor { var Flags: UInt32 var Parent: TargetRelativeDirectPointer<UnsafeRawPointer> var Name: TargetRelativeDirectPointer<CChar> var NumRequirementsInSignature: UInt32 var NumRequirements: UInt32 var AssociatedTypeNames: TargetRelativeDirectPointer<CChar> }

TargetProtocolDescriptor 的結構還原出來後,我們接著也把 TargetProtocolConformanceDescriptor 的結構還原出來,程式碼如下: swift struct TargetProtocolConformanceDescriptor { var `Protocol`: TargetRelativeDirectPointer<TargetProtocolDescriptor> var TypeRef: UnsafeRawPointer var WitnessTablePattern: UnsafeRawPointer var Flags: UInt32 }

4. 驗證還原出來的 witness_table 的記憶體結構

通過上面幾點呢,我們把 witness_table 的記憶體結構還原出來了,還原出來後我們做一個驗證,看看還原的是否正確。

還原出來的完整程式碼如下: ```swift struct ProtoclInstaceStruct { var heapObj: UnsafeRawPointer var unkown1: UnsafeRawPointer var unkown2: UnsafeRawPointer var metadata: UnsafeRawPointer var witness_table: UnsafeMutablePointer }

struct TargetWitnessTable { var protocol_conformance_descriptor: UnsafeMutablePointer var protocol_witness: UnsafeRawPointer }

struct TargetProtocolConformanceDescriptor { var Protocol: TargetRelativeDirectPointer var TypeRef: UnsafeRawPointer var WitnessTablePattern: UnsafeRawPointer var Flags: UInt32 }

struct TargetProtocolDescriptor { var Flags: UInt32 var Parent: TargetRelativeDirectPointer var Name: TargetRelativeDirectPointer var NumRequirementsInSignature: UInt32 var NumRequirements: UInt32 var AssociatedTypeNames: TargetRelativeDirectPointer }

struct TargetRelativeDirectPointer { var RelativeOffset: Int32

mutating func getmeasureRelativeOffset() -> UnsafeMutablePointer<Pointee>{
    let offset = self.RelativeOffset

    return withUnsafePointer(to: &self) { p in
        return UnsafeMutablePointer(mutating: UnsafeRawPointer(p).advanced(by: numericCast(offset)).assumingMemoryBound(to: Pointee.self))
    }
}

} ```

下面是我的驗證程式碼: ```swift var c2: Shape = Circle(20)

withUnsafePointer(to: &c2) { c2_ptr in c2_ptr.withMemoryRebound(to: ProtoclInstaceStruct.self, capacity: 1) { pis_ptr in print(pis_ptr.pointee)

    let protocolDesPtr = pis_ptr.pointee.witness_table.pointee.protocol_conformance_descriptor.pointee.Protocol.getmeasureRelativeOffset()
    print("協議名稱:\(String(cString: protocolDesPtr.pointee.Name.getmeasureRelativeOffset()))")
    print("協議方法的數量:\(protocolDesPtr.pointee.NumRequirements)")
    print("witnessMethod:\(pis_ptr.pointee.witness_table.pointee.protocol_witness)")
}

} swift 列印結果: ProtoclInstaceStruct(heapObj: 0x000000010732a1c0, unkown1: 0x0000000000000000, unkown2: 0x0000000000000000, metadata: 0x00000001000081f0, witness_table: 0x0000000100004088) 協議名稱:Shape 協議方法的數量:1 witnessMethod:0x00000001000021d0 ```

我們在分析 IR 程式碼的時候,應該有注意到 TargetWitnessTable 的 protocol_witness,這一個其實儲存的就是我們的 witnessMethod,在上面的 IR 程式碼中其實已經寫的很清楚了,但我們還是來驗證一下。

  • 在終端使用 nm -p <可執行檔案> | grep <記憶體地址> 打印出這個方法的符號資訊。
  • 接著用 xcrun swift-demangle <符號資訊> 還原這個符號資訊。

如圖: nm -p 還原符號資訊.png

所以,這個協議見證表(witness_table)的本質其實就是 TargetWitnessTable。第一個元素儲存的是一個 descriptor,記錄協議的一些描述資訊,例如名稱和方法的個數等。那麼從第二個元素的指標開始儲存的就是函式的指標

注意!ProtoclInstaceStruct 中的 witness_table 變數是一個連續的記憶體空間,所以這個 witness_table 變數存放的可能是很多個協議的見證表。

存放多個協議見證表的因素取決於變數的靜態型別,如果這個變數的型別是協議組合型別,那麼 witness_table 存放的就是協議組合中所有協議的見證表,如果這個變數的型別是指定單獨的某個協議,那麼 witness_table 存放的只有這個協議的見證表。

四、Existential Container

我們在第三大點中研究的物件一直是協議的見證表(witness_table),那麼在這個探索的過程,我們曾經還原出 c2 例項的記憶體佈局,也就是 ProtoclInstaceStruct 這個結構。這個是什麼呢,我們來介紹一個東西 - Existential Container

Existential Container: 它是編譯器生成的一種特殊的資料型別,用於管理遵守了相同協議的協議型別,因為這些型別的記憶體大小不一致,所以通過當前的 Existential Container 統一管理。 - 對於小容量的資料,直接儲存在 Value Buffer。

  • 對於大容量的資料,通過堆區分配,儲存堆空間的地址。

想說明白的一點就是還原出來的 ProtoclInstaceStruct 其實就是 Existential Container,翻譯過來叫做存在容器。這個存在容器最後的兩個 8 位元組儲存的內容是固定的,儲存的是這個例項型別的元型別和協議的見證表。

那前面的 24 個位元組用來存放什麼: - 如果這個例項是引用型別,那麼第一個 8 位元組儲存的就是例項在堆空間的地址值。

  • 如果這個例項是值型別,當著 24 個位元組可以完全儲存值型別的記憶體(也就是值型別的屬性值),那麼它就直接儲存在這 24 個位元組裡。如果超出了 24 個位元組,會通過堆區分配,然後第一個 8 位元組儲存堆空間的地址。

所以 ProtoclInstaceStruct 的結構應該是這樣的: swift struct ExistentialContainer { var valueBuffer1: UnsafeRawPointer var valueBuffer2: UnsafeRawPointer var valueBuffer3: UnsafeRawPointer var metadata: UnsafeRawPointer var witness_table: UnsafeRawPointer }

接下來我們通過一個結構體來驗證,為了方便測試,我們還是那之前的 Circle 稍微改一下,程式碼如下: ```swift struct Circle: Shape { var radius = 10 var width: Int var height: Int

init(_ radius: Int) {
    self.radius = radius
    self.width = radius * 2
    self.height = radius * 2
}

var area: Double {
    get {
        return Double(radius * radius) * 3.14
    }
}

}

var c2: Shape = Circle(10) print("end") ```

我們來看一下它的記憶體佈局,如圖:

值型別的存在容器.png

此時存在容器的前 24 個位元組分別儲存著 Circle 的 radius、width 和 height。接下來我新增一個屬性 height1,程式碼如下: ```swift struct Circle: Shape { var radius: Int var width: Int var height: Int var height1: Int

init(_ radius: Int) {
    self.radius = radius
    self.width = radius * 2
    self.height = radius * 2
    self.height1 = radius * 2
}

var area: Double {
    get {
        return Double(radius * radius) * 3.14
    }
}
var area1: Double {
    get {
        return Double(radius * radius) * 3.14
    }
}

} ```

我們來看一下它的記憶體佈局,如圖:

值型別的存在容器超出.png

如圖所示,這就驗證了前面對存在容器的概念和意義。