Swift記憶體賦值探索一: 理解物件在記憶體中的儲存狀態
這系列記錄的文章是由一個實際需求引發的 —— 我們能夠在 OC 中暢快的使用 KVC ,而在 Swift 中,如果想要使用 KVC ,那麼操作物件應該直接或者間接繼承 NSObject 。原因是 KVC 屬於 Runtime 中的內容,而 Runtime 專屬於 OC ,所以我們要使用 KVC 就必須使用 OC 的類來進行。
KVC 有哪些好處我就不多說,越是深入到 Runtime 中越能體會到它的強大。一個簡單的例子就足以說明 —— 如果我們希望對一個例項物件的所有屬性賦值,除了最簡單而又最繁瑣的直接使用.
語法逐個賦值之外,還有一種更加簡單的方式:通過 Runtime 獲取物件所有屬性的集合,然後遍歷這個屬性,使用 KVC 統一賦值 —— 這可以寫成一個分類,最後暴露的僅僅是一個介面了。
如果 Swift 要做到這一步該怎麼做? 本人不才,除了繼承 OC 類使用 KVC ,並沒有找到其他現成的方案去實現純 Swift 的 “KVC” 。好吧,這確實是一個悲傷的故事。
而這一切並非沒有轉機:直到我使用 HandySwift 這個Json 和 Swift 物件互轉的框架之後,不僅感嘆作者的思路開闊,當然更多的是驚喜,在純 Swift 中實現 KVC 就靠它了: HandySwift 使用了記憶體賦值的方式,在“暗地裡”給純 Swift 類的例項物件賦值,避免了開發者面臨非 OC 物件需要逐個點語法賦值的尷尬。這不正是另形式的 “KVC” 嗎?
這一系列文章就如何實現 記憶體賦值 進行的探究做了一個記錄。
1. 物件如何存在記憶體中
我們申請一個物件之後,系統會為它分配記憶體,至於分配在哪裡,這將視情況而定,一般來說,區域性變數等都會放在棧區,它的作用域很小。 而一個常量或者全域性變數則一般會在堆中。我們今天討論的主角是 Swift 中的例項物件的堆存賦值情況,大多數的使用場景是在全域性的狀態下,也即是在堆區討論。 其實就算是在棧區也是一樣的,只要物件還存活,我們只需要獲取它的地址,然後賦值就達到了我們的目的。
Swift 中,記憶體有三種狀態:
- 未繫結型別同時未初始化值
- 繫結型別但是未初始化值
- 繫結型別同時初始化值
如果我們申請一個物件,除了基礎物件之外,複雜的物件都會申請一個指向它的記憶體的指標,我們通過指標區訪問這個記憶體中的值。但是為了正確的得到被訪問值所佔空間的真實大小,還需要指定這個記憶體的型別,以便快速得到記憶體中的值。
在 Swift 中,任何一個存活的物件都有對應的儲存記憶體,這個記憶體初始化的值就是該物件,而相對應的記憶體的型別就是物件的型別。 在初始化物件的時候,系統根據物件屬性的型別和記憶體對齊的規則計算物件記憶體的大小,之後,物件的屬性修改也將在各個屬性的地址上做修改。
比如,如果有一個 Person 類:
class Person {
var name: String!
let isMan: Bool!
var age: Int!
var hight:Double!
init...
}
let p = Person()
物件p
被初始化之後,我們分析一下它的記憶體。
本身p
就是一個指標,這個指標的型別是Person,記憶體中的值就則為p
的內容。而在這個指標p
指向的區域中,存放name
的那部分記憶體的型別應該是一個String型別,其內部的值為name
指標,除了String是指標之外,其餘的都是基本資料型別, 他們對應的記憶體的型別是各自的型別,並且值則為各自的值。
假設String 型別佔2個位元組,Bool 佔 1 個位元組,Int 佔4個位元組,Double 8個位元組。
如果不考慮記憶體對齊的話,那麼 p 佔用的記憶體應該為15個連續位元組。排列看起來像這樣:
這樣緊密排布,看似很節省空間,但是因為 CPU 定址的方式的特點,這種連續儲存的方式,會導致訪問的效率急劇下降。因此,實際上對於程式語言來說,還需要進行專門的記憶體使用設計 —— 記憶體佈局,以便提升 CPU 的訪問效率,其中記憶體對齊就是其一。
假如,Swift在64位裝置上記憶體對齊模數為 8 ,對於p來說,它的真實記憶體佈局應該是這樣的:
也即是一共需要 32位元組的記憶體空間。跟我們想象中的不太一樣,實際上假如有屬性出現可選類的時候,記憶體的佔用將更大:比如將
hight
屬性的型別更改為 Double?,這時候他將佔用 9 個位元組,如果算上記憶體對齊,它需要的實際空間是 16 個位元組。其他的各種記憶體佈局方式跟程式語言的記憶體模型有關。
2. 如何給記憶體中的物件的屬性賦值
僅僅知道每個物件的儲存原則是不夠的,我們不知道物件的每個屬性是如何在物件記憶體區域儲存的。當我們已經深入到了記憶體儲存的時候,有關的類的大部分資訊就不再能夠使用正常的渠道獲取。比如,儘管我們知道某一塊的記憶體中存了一些我們想要的資訊。但是我們並不夠準確知道,每一個地址區域對應的內容是什麼。
好在,Swift 為型別物件保留了一個關於物件的記憶體佈局描述 —— MemoryLayout。
當使用指標分配或繫結記憶體時,可以使用MemoryLayout作為關於型別資訊的來源。
再看看MemoryLayout中的一些內容:
static var alignment: Int
The default memory alignment of `T`, in bytes.
static var size: Int
The contiguous memory footprint of `T`, in bytes.
static var stride:<wbr> Int
The number of bytes from the start of one instance of `T` to the start of the next when stored in contiguous memory or in an `Array<T>`.
內部包含了一個物件的佔用空間: size
, 實際長度 stride
, 以及對齊模數 alignment
。MemoryLayout很有用,我們通過這個類對不同的裝置做記憶體佈局的適配。有了這個三個屬性,我們可以對一個物件基本可以確定一個物件在不同的裝置上的佔用記憶體資訊。但是這些資訊依然不夠,因為這個記憶體佈局並沒有體現每一個屬性的記憶體佈局資訊。
HandySwift 的作者在實現記憶體賦值的時候也遇到了這個問題,他通過總結髮現 Swift 中,一個類例項的屬性記憶體佈局是有規律的:
32位機器上,類前面有4+8個位元組儲存meta資訊,64位機器上,有8+8個位元組;
記憶體中,欄位從前往後有序排列;
如果該類繼承自某一個類,那麼父類的欄位在前;
Optional會增加一個位元組來儲存.None/.Some資訊;
每個欄位需要考慮記憶體對齊;(包括 meta 資訊)
作者:xycn
連結:https://www.jianshu.com/p/eac4a92b44ef
來源:簡書
這個規律在 HandySwift 中使用了很久,並且執行良好,我們可以認為上述規律是可行的。
這其中有個問題:如何獲取物件的全部屬性,這裡注意要包含父類的繼承屬性。Swift 提供了反射機制 Mirro ,通過 Mirro 我們能夠逐一獲取到物件的所有屬性資訊,包含了屬性的值和型別,唯一遺憾是我們並不能對屬性賦值(不然這篇文章可以燒掉了)。並且這個資訊是從父類開始,按欄位有序排列,剛好符合我們的規律需求。
至於如何賦值就比較簡單了,使用指標賦值就可以了。儘管 Swift 不推薦開發者使用指標,但是並非不能使用,詳細的內容在後面的文中會介紹。
到這裡,對於如何在賦值我們已經有了思路:
- 找到一個物件的起始地址,捨去 meta 相關的無用地址。找到第一個有效的地址。
- 通過遍歷 Mirro 反射得到的屬性列表,按照每個屬性的型別計算它所佔的位元組數,注意,這裡要考慮到記憶體對齊的情況,不然會出事。同時因為有了首地址,後面的地址逐個累加過去即可。
- 使用指標給對應的記憶體賦值。
- 接下來就是迴圈2、3。 直到屬性列表中所有的屬性全部賦值完畢。
以上思路完成了對一個物件的所有屬性通過記憶體賦值的全過程。 實際上的操作肯定比這個要複雜的多了。 我們下篇文章中揭曉。
相關文章
- 3 python的數值在記憶體中如何儲存Python記憶體
- 記憶體中的資料儲存記憶體
- 小數在記憶體中是如何儲存的?記憶體
- 【C語言】整型在記憶體中的儲存C語言記憶體
- InnoDB儲存引擎——記憶體儲存引擎記憶體
- JAVA物件在JVM中記憶體分配Java物件JVM記憶體
- double型別資料在記憶體中中儲存格式型別記憶體
- 動態記憶體的賦值和修改(Android之JNI)記憶體賦值Android
- memcached全面剖析--2.理解memcached的記憶體儲存記憶體
- 深圳Java培訓:Java中的float在記憶體中的儲存Java記憶體
- Redis 記憶體優化神技,小記憶體儲存大資料Redis記憶體優化大資料
- 簡單理解動態記憶體分配和靜態記憶體分配的區別記憶體
- [譯] Swift 中的記憶體洩漏Swift記憶體
- Swift 中的記憶體管理詳解Swift記憶體
- C/C++浮點數在記憶體中的儲存方式C++記憶體
- Swift列舉關聯值的記憶體探究Swift記憶體
- JS中的棧記憶體、堆記憶體JS記憶體
- 物件記憶體圖物件記憶體
- 儲存類別和記憶體管理記憶體
- 探索Java記憶體模型Java記憶體模型
- 探索iOS記憶體分配iOS記憶體
- 記憶體--通俗理解記憶體
- Swift 閉包中的記憶體洩漏Swift記憶體
- ABAP Memory Inspector 裡對動態記憶體物件的記憶體消耗度量方式記憶體物件
- linux記憶體管理(一)實體記憶體的組織和記憶體分配Linux記憶體
- Fdmemtable 記憶體表儲存圖片的例子記憶體
- 資料在記憶體中儲存的方式:大端模式與小端模式記憶體模式
- 理解JVM(一):記憶體結構JVM記憶體
- 在Linux中,記憶體怎麼看?磁碟狀態怎麼看?Linux記憶體
- JVM中java例項物件在記憶體中的佈局JVMJava物件記憶體
- 什麼是Java記憶體模型(JMM)中的主記憶體和本地記憶體?Java記憶體模型
- Redis 記憶體使用優化與儲存Redis記憶體優化
- 物件的記憶體佈局物件記憶體
- Memory記憶體傳值記憶體
- 理解 iOS 的記憶體管理iOS記憶體
- 【記憶體洩漏和記憶體溢位】JavaScript之深入淺出理解記憶體洩漏和記憶體溢位記憶體溢位JavaScript
- Java物件記憶體模型Java物件記憶體模型
- Java 物件記憶體分析Java物件記憶體