首先我們來看一段代碼
protocol DrawProtocol {
func draw()
}
class Student: DrawProtocol {
var x: Int = 0
var y: Int = 0
func draw() {
}
}
struct Point: DrawProtocol {
var x: Int = 0
var y: Int = 0
func draw() {
}
}
let p = Point()
let s = Student()
let draws: [DrawProtocol] = [p,s]
那么請問各位看官, draws中存儲的是什么呢?
事實上短曾,在這種情況下寒砖,變量 draws 中存儲的元素是一種特殊的數(shù)據(jù)類型:Existential Container。因為: 無法確定 p , s 的內(nèi)存大小!
Existential Container
Existential Container是編譯器生成的一種特殊的數(shù)據(jù)類型嫉拐,用于管理遵守了相同協(xié)議的協(xié)議類型哩都。因為這些數(shù)據(jù)類型的內(nèi)存空間尺寸不同,使用 Extential Container 進行管理可以實現(xiàn)存儲一致性婉徘。
結(jié)構(gòu)如下:
首位3個詞作為 Value Buffer漠嵌, 每個詞包含8個字節(jié) (存儲的可能是值,也可能是指針)
Small Value(存儲空間小于等于 Value Buffer)盖呼,可以直接內(nèi)聯(lián)存儲在 Value Buffer 中儒鹿。
Large Value(存儲空間大于 Value Buffer),當(dāng)值的數(shù)量大于3個屬性或者總尺寸超過valueBuffer的占位几晤,則會在堆區(qū)分配內(nèi)存進行存儲约炎,Value Buffer 只存儲對應(yīng)的指針, 指針指向了堆空間 (Swift 采用了 Indirect Storage With Copy-On-Write 技術(shù)進行了優(yōu)化。)
Copy-On-Write 這種技術(shù)可以提高內(nèi)存指針利用率蟹瘾,降低堆區(qū)內(nèi)存消耗圾浅,從而實現(xiàn)性能提升。該技術(shù)的原理是:拷貝時僅僅拷貝 Extension Container憾朴,當(dāng)修改值時狸捕,先檢測引用計數(shù),如果引用計數(shù)大于 1伊脓,則開辟新的堆區(qū)內(nèi)存
1 個詞作為 Value Witness Table (管理協(xié)議類型的生命周期)
由于協(xié)議類型的具體類型不同府寒,其內(nèi)存布局也不同,Value Witness Table 則是對協(xié)議類型的生命周期進行專項管理报腔,從而處理具體類型的初始化株搔、拷貝、銷毀纯蛾。
1 個詞作為 Protocol Witness Table (管理協(xié)議類型的方法調(diào)用)
在 Class 中纤房,基于繼承關(guān)系的多態(tài)是通過 Virtual Table 實現(xiàn)的;在 POP 中翻诉,沒有繼承關(guān)系炮姨,因為無法使用 Virtual Table 實現(xiàn)基于協(xié)議的多態(tài),取而代之的是 Protocol Witness Table碰煌。每個結(jié)構(gòu)體會創(chuàng)造Protocol Witness Table表中舒岸,內(nèi)部包含指針,指向方法!
內(nèi)存分布如下:
1. payload_data_0 = 0x0000000000000004,
2. payload_data_1 = 0x0000000000000000,
3. payload_data_2 = 0x0000000000000000,
4. instance_type = 0x000000010d6dc408 ExistentialContainers`type
metadata for ExistentialContainers.Car,
5. protocol_witness_0 = 0x000000010d6dc1c0
ExistentialContainers protocol witness table for
ExistentialContainers.Car:ExistentialContainers.Drivable
in ExistentialContainers
在Swift編譯器中芦圾,通過Existential Container實現(xiàn)的偽代碼如下:
func drawACopy(local :Drawable) {
local.draw()
}
let val :Drawable = Point()
drawACopy(val)
//existential container的偽代碼結(jié)構(gòu)
struct ExistContDrawable {
var valueBuffer:(Int, Int, Int)
var vwt:ValueWitnessTable
var pwt:DrawableProtocolWitnessTable
}
// drawACopy方法生成的偽代碼
func drawACopy(val:ExistContDrawable) { //將existential container傳入
var local = ExistContDrawable() //初始化container
let vwt = val.vwt //獲取value witness table蛾派,用于管理生命周期
let pwt = val.pwt //獲取protocol witness table,用于進行方法分派
local.type = type
local.pwt = pwt
vwt.allocateBufferAndCopyValue(&local, val) //vwt進行生命周期管理个少,初始化或者拷貝
pwt.draw(vwt.projectBuffer(&local)) //pwt查找方法洪乍,這里說一下projectBuffer,因為不同類型在內(nèi)存中是不同的(small value內(nèi)聯(lián)在棧內(nèi)夜焦,large value初始化在堆內(nèi)壳澳,棧持有指針),所以方法的確定也是和類型相關(guān)的茫经,我們知道巷波,查找方法時是通過當(dāng)前對象的地址,通過一定的位移去查找方法地址科平。
vwt.destructAndDeallocateBuffer(temp) //vwt進行生命周期管理褥紫,銷毀內(nèi)存
}
Protocol Type 存儲屬性
在Swift中class的實例和屬性都存儲在堆區(qū),Struct實例在棧區(qū)! 如果包含指針屬性則存儲在堆區(qū)瞪慧,Protocol Type如何存儲屬性髓考?
小的數(shù)據(jù)則通過Existential Container內(nèi)聯(lián)實現(xiàn)。那么存在堆區(qū)的數(shù)據(jù)弃酌,又是如何處理Copy呢氨菇?
protocol Drawable { func draw() }
class Point {
var x1: CGFloat = 0
var x2: CGFloat = 0
var y1: CGFloat = 0
var y2: CGFloat = 0
}
struct Student: Drawable {
var p: Point
func draw() { }
}
let s1 = Student(p: Point())
let s2 = s1
將新的Exsitential Container的valueBuffer指向同一個value即創(chuàng)建指針引用,但是如果要改變值怎么辦?我們知道Struct值的修改和Class不同妓湘,Copy是不應(yīng)該影響原實例的值的!
這里用到了一個技術(shù)叫做Indirect Storage With Copy-On-Write查蓉,即優(yōu)先使用內(nèi)存指針。通過提高內(nèi)存指針的使用榜贴,來降低堆區(qū)內(nèi)存的初始化豌研。降低內(nèi)存消耗妹田。在需要修改值的時候,會先檢測引用計數(shù)檢測鹃共,如果有大于1的引用計數(shù)鬼佣,則開辟新內(nèi)存,創(chuàng)建新的實例霜浴。在對內(nèi)容進行變更的時候晶衷,會開啟一塊新的內(nèi)存。
偽代碼如下:
struct Line :Drawable {
var storage : Point
init() {
storage = Point()
}
func draw() { }
mutating func move() {
// 如過存在多份引用阴孟,則開啟新內(nèi)存晌纫,否則直接修改
if !isUniquelyReferencedNonObjc(&storage) {
storage = Point(storage) //柯里化
}
}
}
這樣實現(xiàn)的目的:通過多份指針去引用同一份地址的成本遠遠低于開辟多份堆內(nèi)存。
靜態(tài)多態(tài) Static Polymorphism
protocol Drawable {
func draw()
}
struct Line: Drawable {
var x = 0
func draw() {}
}
struct Point: Drawable {
var y = 0
func draw() {}
}
func drawACopy(local :Drawable) {
local.draw()
}
let line = Line()
drawACopy(line)
let point = Point()
drawACopy(point)
關(guān)于 Virtual Table 和 Protocol Witness Table 的區(qū)別永丝,個人理解:
它們都是一個記錄函數(shù)地址的列表(即函數(shù)表)锹漱,只是它們的生成方式是不同的。
對于 Virtual Table慕嚷,在編譯時凌蔬,子類的函數(shù)表是通過對父類函數(shù)表進行拷貝、覆寫闯冷、插入等操作生成的砂心。
對于 Protocol Witness Table,在編譯時蛇耀,函數(shù)表是通過識別當(dāng)前類型對協(xié)議的實現(xiàn)辩诞,直接生成的。