主要內(nèi)容
講解了Swift中對引用類型和值類型的內(nèi)存管理方式,并提出一些優(yōu)化建議。
影響性能的重要因素
- 棧內(nèi)存 vs 堆內(nèi)存
棧內(nèi)存的分配和釋放僅僅是通過移動棧指針來實現(xiàn)的。而堆內(nèi)存則要根據(jù)申請的大小在堆中尋找合適的位置打颤,釋放時需要將內(nèi)存塊回收到堆中,并且要考慮線程安全漓滔,所以會慢得多瘸洛。Swift中的各種基本數(shù)據(jù)類型和容器(如Int, String, Array, Dictionary等)都是struct
,編譯器會盡可能用棧的方式來為它們分配內(nèi)存次和。 - 引用計數(shù)
編譯器會自動在我們的代碼中加入retain
和release
的調(diào)用反肋。這兩個函數(shù)的調(diào)用頻率極高且涉及線程安全的控制,使其有可能成為性能瓶頸踏施。 - 動態(tài)函數(shù)調(diào)用 vs 靜態(tài)函數(shù)調(diào)用
動態(tài)函數(shù)調(diào)用就是在運行時才確定函數(shù)名對應(yīng)的函數(shù)地址石蔗。對于動態(tài)調(diào)用,程序必須動態(tài)地查詢函數(shù)地址畅形,而無法在編譯期使用inline等優(yōu)化手段养距,會相對慢一些。
優(yōu)化
以struct
作為Dictionary
的key
此處把3個數(shù)據(jù)點序列化為一個String
日熬,并以此為key棍厌。這樣做有兩個缺點:
- 使用
String
為key意味著可以傳入任意String
,哪怕不是這3個數(shù)據(jù)點的序列化結(jié)果竖席,比如"abc"或"123"耘纱。 -
String
本身雖然是struct
,但它內(nèi)部儲存字符的數(shù)組卻是分配在堆上的毕荐。每次序列化意味著一次堆操作束析。
改為struct
可以優(yōu)雅地解決這兩個問題。
盡量使用(純)值類型
值類型如果包含引用類型的數(shù)據(jù)成員憎亚,則程序仍需為此數(shù)據(jù)成員進行引用計數(shù)的維護和堆操作员寇。
所以弄慰,盡可能地使用“純值”類型(這是我造的詞,表示不包含引用類型的值類型蝶锋,編譯器可以實現(xiàn)完全的棧內(nèi)存管理)陆爽。比如以下結(jié)構(gòu)中,有兩個String
成員扳缕,程序需為它們進行堆操作墓陈。
把它改成UUID
和enum
類型
注意:此處的enum
的源數(shù)據(jù)類型雖然是String
,但是它在內(nèi)存并不是持有一個String
(雖然沒說第献,我想應(yīng)該只是個Int)贡必。
所以整個結(jié)構(gòu)變成了純值類型。
感謝 小剛_aea8 的訂正庸毫。此處的
Attachment
由于還包含了URL
類型的成員仔拟,所以它并沒有變成一個“純值”類型。
在不需要多態(tài)時飒赃,使用泛型代替protocol
先說下多態(tài)的基本實現(xiàn)原理利花。
上面的代碼?是通過override
的方式來實現(xiàn)多態(tài)的。編譯器為每個對象添加了一個type
成員载佳,里面是這個具體類的信息(稱為Virtual Method Table炒事,簡稱V-Table),其中就包含了它override
的函數(shù)指針蔫慧,也就是各自的draw
函數(shù)挠乳。下面調(diào)用的代碼就是通過查詢V-Table才找到正確的實現(xiàn)的。
上面的代碼把class
改為了protocol
和struct
姑躲。
像前一個例子中那樣睡扬,class
類型的數(shù)組的每個成員只占用一個?指針的size,因為只需放入一個指針黍析。而這個例子中卖怜,由于struct
是值類型,它被放進數(shù)組時應(yīng)該是拷貝進去的阐枣,所以理論上马靠,它要占用struct
自身size的內(nèi)存。那不同size的struct
如何被放進同一個數(shù)組呢蔼两?
當(dāng)需要用protocol
類型指針來?指向struct
時甩鳄,Swift采用了一個叫Existential Container的結(jié)構(gòu)來保存?struct
的成員變量和方法。如下圖:
Existential Container的前3個word
稱為value buffer
宪哩,是用來保存struct
的數(shù)據(jù)成員的娩贷。對于比較小的struct
可以直接把值塞進去第晰,對于超過3個word
的struct
锁孟,則只能分配在堆上彬祖,然后在這里保存一個指針。此時每個struct
在棧上占用的空間就一樣了(5個word
)品抽,這就解答了上面的問題储笑。接下來,為了統(tǒng)一處理不同size的struct
圆恤,又在第4個word
增加了一個叫Value Witness Table(簡稱VWT)的結(jié)構(gòu)突倍,里面包含了一組函數(shù)。如下圖:
以Point
和Line
為例:
函數(shù) | Point | Line |
---|---|---|
allocate | 沒動作 | 在堆上分配內(nèi)存盆昙,并保存指針到value buffer
|
copy | 把值拷到value buffer
|
把值拷到value buffer 中的指針對應(yīng)的堆內(nèi)存上 |
destruct | 如果包含引用類型的成員變量羽历,這里需要?引用減1。此處沒有淡喜,所以沒動作 | 也沒動作 |
deallocate | 沒動作 | 釋放堆上的內(nèi)存 |
注:實際上不止這幾個函數(shù)秕磷,還有allocateBufferAndCopyValue
、projectBuffer
炼团、destructAndDeallocateBuffer
等澎嚣。
在第5個word
上添加一個Protocol Witness Table(簡稱PWT)的結(jié)構(gòu)。里面包含protocol
的成員方法的指針瘟芝。PWT與V-Table很類似易桃,程序也是通過查詢這個表來實現(xiàn)多態(tài)的。
上圖中锌俱,左上角是源代碼晤郑,左下角是編譯時產(chǎn)生的代碼∶澈辏可以看到:
- 類型為
Drawable
的參數(shù)編譯后變成ExistContDrawable
贩汉,也就是上面提到的Existential Container。 - 函數(shù)首先在創(chuàng)建一個
ExistContDrawable
類型的臨時變量锚赤,用來放參數(shù)的值匹舞。 - 拷貝
type
成員(里面包含實現(xiàn)類的信息,圖中寫錯了线脚,應(yīng)該是 local.type = val.type) - 拷貝
pwt
成員(里面包含struct
實現(xiàn)的protocol
中的方法的函數(shù)地址) - 分配空間并賦值
- 調(diào)用
projectBuffer
取出數(shù)據(jù)正確的內(nèi)存地址(里面判斷是否需要堆操作赐稽。我想應(yīng)該是從前面的type
里面取出實現(xiàn)類的size來判斷的。) - 調(diào)用
draw
函數(shù)浑侥。聲明中的draw
方法雖然沒有參數(shù)姊舵,編譯出來后會加上一個參數(shù),就是結(jié)構(gòu)體的實際內(nèi)存地址寓落。 - 最后調(diào)用
destructAndDeallocateBuffer
清理內(nèi)存(temp寫錯了括丁,應(yīng)該是local)
一個簡單的調(diào)用實際做了這么多事情。這些代價都是花在需要動態(tài)判斷具體struct
的信息和跳轉(zhuǎn)到?對應(yīng)的方法上的伶选。如果改成使用泛型史飞,則編譯器就可以在編譯期知道具體類型了尖昏,也就可以進行諸如inline等優(yōu)化手段。具體實現(xiàn)?參考下面兩張圖:
但是构资,這樣做的前提是不需要使用多態(tài)抽诉。如果像上面的例子那樣,需要把不同的實現(xiàn)類放進一個數(shù)組中吐绵,則必須借用多態(tài)了迹淌。
使用“寫時拷貝”
由于struct
是值類型,當(dāng)它在傳遞時會發(fā)生多次拷貝己单。如果你的struct
拷貝成本很高或者拷貝發(fā)生得很頻繁唉窃,而修改卻很少的話,可以考慮使用“寫時拷貝”的方法來優(yōu)化它纹笼,如下:
此處使用一個storage
的class
來包裝數(shù)據(jù)句携。當(dāng)Line
發(fā)生拷貝時,storage
成員只發(fā)生引用計數(shù)加一的操作允乐。當(dāng)需要真正寫入時矮嫉,再調(diào)用isUniquelyReferencedNonObjc
判斷一下storage
的引用計數(shù)是否大于1, 是的話則顯式拷貝一份再進行寫入牍疏。
內(nèi)置類型String
,Array
,Set
,Dictionary
均使用了這個技術(shù)蠢笋。