iOS-Swift 獨孤九劍:八、協議的本質
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 ```
-
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(:) 方法的呼叫,如圖:
注意看,born(:) 的型別在 sil 中是 class_method 型別的,在 SIL參考文件 中有介紹,class_method 型別的方法是通過 VTable 查詢的,如圖:
接下來我們看到 main.sil 檔案最底部的程式碼,如圖:
可以看到,born(:) 確實是儲存在 VTable 當中了,但是下面的 witness_table 是用來幹啥的,並且裡面也有一個 born(:),這是個啥。接下來我幹一件事,我把變數 p 宣告為 Born 協議,程式碼如下:
swift
let p: Born = Person()
接下來重新將 main.swift 檔案編譯成 main.sil 檔案,然後直接看 main 函式,如圖:
此時,我們發現函式的型別變了,變成了 witness_method 型別的,我們來看 SIL參考文件 中是如何介紹 witness_method 的:
翻譯如下:
查詢受該協議約束的泛型型別變數的協議方法的實現。結果將在原始協議的 Self 原型上是通用的,並具有 witness_method 呼叫約定。如果引用的協議是 @objc 協議,則結果型別具有 objc 呼叫約定。
啥意思呢,我們全域性搜尋 @protocol witness for main.Born.born(Swift.String) -> (),找到它的實現,如圖:
注意看,它最終還是會去查詢遵守它的類中的 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 函式,如圖:
我們再來看一下彙編程式碼,如圖:
可以看到,結構體呼叫協議方法的方式直接就是函式地址呼叫。當我指定這個變數的型別為 Born 協議的時候,sil main 函式的實現如下:
注意看,這個時候它的型別變成了 witness_method ,我們再來看這個方法對應的 witness_method 的實現,如圖:
可以看到,它最終還是找到了結構體 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,如圖:
可以看到,針對於第二種情況,它直接就是拿到 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
我們通過 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 的記憶體佈局,並且我們通過 expr -f float -- <記憶體地址>
表示式打印出了 radius 的值。
我們接下來看 c2 的記憶體佈局,如圖:
注意看: - 第一個 8 位元組的記憶體儲存的依然是堆空間的地址值。
-
第二個和第三個 8 位元組儲存的是啥我們也不知道是什麼。
-
第四個 8 位元組儲存的是堆空間 metadata 的地址。
-
最後的 8 位元組儲存的其實是 witness_table 的地址。
那怎麼知道最後的 8 位元組儲存的就是 witness_table 的地址呢?最後的 8 位元組記憶體地址為 0x0000000100004028,我們接下來開啟彙編除錯,找到 c2 的建立後找到 witness_table 相關的程式碼,如圖:
如圖所示,所以最後的 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 的結構中有兩個成員,那麼根據這個資訊,還原出來的 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,如圖:
注意看,原始碼中的註釋也清楚的寫著這個是一個協議的見證表,並且,此時我們知道第 2 點分析出來的 protocol_conformance_descriptor 是一個 TargetProtocolConformanceDescriptor,找到這個結構的定義,發現它有以下成員,如圖:
我們看 Protocol 這個成員變數,它是一個相對型別指標,其型別的結構為 TargetProtocolDescriptor,相對型別指標在《元型別以及 Mirror 原始碼和 HandyJson 分析還原列舉、結構體、類的 Metadata》這篇文章中有介紹,而且我們已經把這個相對型別指標給還原了出來,我們用的時候直接複製過來就好了。
現在需要還原 TargetProtocolDescriptor 的結構,TargetProtocolDescriptor 是繼承自 TargetContextDescriptor 的,TargetContextDescriptor 我們應該無比的熟悉了,在上面提到的文章中也有介紹。所以,TargetProtocolDescriptor 必然有 Flags 和 Parent 兩個成員變數,我們再看一下它自身有什麼,如圖:
此時此刻,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
struct TargetProtocolConformanceDescriptor {
var Protocol
: TargetRelativeDirectPointer
struct TargetProtocolDescriptor {
var Flags: UInt32
var Parent: TargetRelativeDirectPointer
struct TargetRelativeDirectPointer
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 <符號資訊>
還原這個符號資訊。
如圖:
所以,這個協議見證表(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") ```
我們來看一下它的記憶體佈局,如圖:
此時存在容器的前 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
}
}
} ```
我們來看一下它的記憶體佈局,如圖:
如圖所示,這就驗證了前面對存在容器的概念和意義。