Swift記憶體模型的那點事兒
跟OC類似, Swift提供了MemoryLayout類靜態測量物件大小, 注意是在編譯時確定的,不是執行時哦! 作為Java程式設計師想想如何測量Java物件大小? 參考 Java物件到底有多大?
寫這篇部落格的目的在於說明2個黑科技:
1、 類/結構體的成員變數宣告順序會影響它的佔用空間。 原理是記憶體對齊, 有經驗的碼農會把佔用空間大的變數寫在前面, 佔用空間小的寫在後面。 PS: 大道同源,C/C++/Object-C/Swift/Java都需要位元組對齊; 如果面試時問你記憶體優化有什麼經驗? 你告訴他這個一定會另眼相看!!!
2、 可以篡改Swift結構體/類物件的私有成員(通過指標操作記憶體)。
為什麼要記憶體對齊呢?簡單來說就是CPU定址更快,詳情參見 記憶體對齊原因
在不同機型上資料型別佔用的空間大小也不同, 例如iPhone5上Int佔4個位元組, iPhone7上Int佔8個位元組。 本文是在iPhone7模擬器上驗證的。
Stack(棧),儲存值型別的臨時變數,函式呼叫棧,引用型別的臨時變數指標,結構體物件和類物件引用
Heap(堆),儲存引用型別的例項,例如類物件
Swift3.0提供了記憶體操作類MemoryLayout(注意:Swift跟OC一樣,記憶體排列時需要對齊,造成一定的記憶體浪費,我們稱之為記憶體碎片), 它有3個主要引數:
1、例項方法alignment和靜態方法 alignment(ofValue: T):
位元組對齊屬性,它要求當前資料型別相對於起始位置的偏移必須是alignment的整數倍。 例如在iPhone7上Int佔8個位元組,那麼在類/結構體中Int型引數的起始位置必須是8的整數倍(可認為類/結構體第一個成員變數的記憶體起始位置為0), 後面會用例項說明。
2、 例項成員變數size和靜態方法size(ofValue: T)
得到一個 T 資料型別例項佔用連續記憶體位元組的大小。
3、例項成員變數stride和靜態方法stride(ofValue: T)
在一個 T 型別的陣列中,其中任意一個元素從開始地址到結束地址所佔用的連續記憶體位元組的大小就是 stride。 如圖:
註釋:陣列中有四個 T 型別元素,雖然每個 T 元素的大小為 size 個位元組,但是因為需要記憶體對齊的限制,每個 T 型別元素實際消耗的記憶體空間為 stride 個位元組,而 stride - size 個位元組則為每個元素因為記憶體對齊而浪費的記憶體空間。
所以, 一個物件或變數佔用的空間是由本身大小和偏移組成的! 我們改變不了資料型別本身的大小, 但我們可以儘量的縮小偏移, 後面會講怎麼做!
下面用幾個例項說明:
class People1: NSObject{
var name: String?
}
class People2: NSObject{
var name: String?
var age: Int?
}
let people1 = People1()
let people2 = People2()
people1和people2佔用多大記憶體???
別暈! 這是類物件的引用, 而引用佔用的記憶體空間是固定的,即people1和people2佔用記憶體大小相同, 區別是指向的記憶體空間佔用大小不同!
如果將類改成結構體會怎樣?
struct People1 {
var name: String?
}
struct People2 {
var name: String?
var age: Int?
}
let people1 = People1(name: "zhangsan")
let people2 = People2(name: "zhangsan", age: 1)
結構體是值型別, 在iPhone7上Int佔8個位元組,String佔24個位元組; 所以people1佔用24個位元組,people2佔用32個位元組。類/結構體的成員宣告順序會影響佔用空間,原理是變數要以自身型別的aligment整數倍作為起始地址,不足的話要在前面補齊位元組(即記憶體碎片)。 下面示例說明: 結構體People1和People2的引數相同,區別是先後順序不一致。
//在iPhone7上佔16個位元組
struct People2 {
var enable = false //佔用1個位元組
var age = 1 //Int佔8個位元組, 因為是8位元組對齊,必須以8的整數倍開始,所以前面要補齊7個位元組偏移。
}
//在iPhone7上佔9個位元組
struct People3 {
var age = 1
var enable = false //aligmnet是1, 所以不要新增偏移
}
print("People2: size=\(MemoryLayout<People2>.size) align=\(MemoryLayout<People2>.alignment) stride=\(MemoryLayout<People2>.stride)")
print("People3: size=\(MemoryLayout<People3>.size) align=\(MemoryLayout<People3>.alignment) stride=\(MemoryLayout<People3>.stride)")
輸出:
People2: size=16 align=8 stride=16
People3: size=9 align=8 stride=16
Optional即可選資料型別會增加1個位元組空間, 由於記憶體對齊的原因,Optional可能佔用更多的記憶體空間。下面以Int為例:
print("Int: size=\(MemoryLayout<Int>.size) align=\(MemoryLayout<Int>.alignment) stride=\(MemoryLayout<Int>.stride)")
print("Optional Int: size=\(MemoryLayout<Optional<Int>>.size) align=\(MemoryLayout<Optional<Int>>.alignment) stride=\(MemoryLayout<Optional<Int>>.stride)")
Int: size=8 align=8 stride=8Optional Int: size=9 align=8 stride=16
Optional Int的size是9, Int的size是8。
空類和空結構體佔多大空間呢?
class EmptyClass {
//佔用一個引用的大小
}
struct EmptyStruct {
//佔1個位元組,因為需要唯一的地址
}
print("EmptyClass: size=\(MemoryLayout<EmptyClass>.size) align=\(MemoryLayout<EmptyClass>.alignment) stride=\(MemoryLayout<EmptyClass>.stride)")
print("EmptyStruct: size=\(MemoryLayout<EmptyStruct>.size) align=\(MemoryLayout<EmptyStruct>.alignment) stride=\(MemoryLayout<EmptyStruct>.stride)")
EmptyClass: size=8 align=8 stride=8EmptyStruct: size=0 align=1 stride=1
EmptyClass佔用8個位元組(跟引用佔用空間大小相等), 即使在EmptyClass新增幾個成員變數, 得到的size仍然是8, 其實這裡實際上測量的是引用; EmptyStruct的size為0但stride為1, 說明佔用1個位元組空間,因為每個例項都需要唯一的地址。
如果你想知道類到底佔用多大記憶體, 那麼你可以嘗試改為結構體後測量一下! 因為你測量的是類的引用。
相信你對Swift記憶體佔用情況有了一定的理解, 現在說說如何篡改記憶體。Swift提供了UnSafePointer類操作指標( 還記得Java怎樣操作指標嗎?看我前面的部落格), iOS不負責UnSafePointer指向記憶體的回收(如果呼叫了allocate方法,則要呼叫deinitialize和deallocate方法釋放記憶體), 所有在使用它時要注意回收記憶體。下面是Swift的所有指標操作類:
如果你想操作類/結構體的記憶體,可以繼承於_PropertiesMetriczable或對應函式。 下面程式碼摘自HandyJSON:
extension _PropertiesMetrizable {
// locate the head of a struct type object in memory
mutating func headPointerOfStruct() -> UnsafeMutablePointer<Byte> {
return withUnsafeMutablePointer(to: &self) {
return UnsafeMutableRawPointer($0).bindMemory(to: Byte.self, capacity: MemoryLayout<Self>.stride)
}
}
// locating the head of a class type object in memory
mutating func headPointerOfClass() -> UnsafeMutablePointer<Byte> {
let opaquePointer = Unmanaged.passUnretained(self as AnyObject).toOpaque()
let mutableTypedPointer = opaquePointer.bindMemory(to: Byte.self, capacity: MemoryLayout<Self>.stride)
return UnsafeMutablePointer<Byte>(mutableTypedPointer)
}
// memory size occupy by self object
static func size() -> Int {
return MemoryLayout<Self>.size
}
// align
static func align() -> Int {
return MemoryLayout<Self>.alignment
}
// Returns the offset to the next integer that is greater than
// or equal to Value and is a multiple of Align. Align must be
// non-zero.
static func offsetToAlignment(value: Int, align: Int) -> Int {
let m = value % align
return m == 0 ? 0 : (align - m)
}
}
使用結構體親測一下篡改私有變數(原理: 拿到物件頭指標、判斷出成員變數的偏移和佔用記憶體大小,然後寫記憶體):
//在iPhone7上測試
struct Pig {
private var count = 4 //8位元組
var name = "Tom" //24位元組
//返回指向 Pig 例項頭部的指標
mutating func headPointerOfStruct() -> UnsafeMutablePointer<Int8> {
return withUnsafeMutablePointer(to: &self) {
return UnsafeMutableRawPointer($0).bindMemory(to: Int8.self, capacity: MemoryLayout<Pig>.stride) }
}
func printA() {
print("Animal a:\(count)")
}
}
var pig = Pig()
let pigPtr: UnsafeMutablePointer<Int8> = pig.headPointerOfStruct() //頭指標, 型別同const void *
//有了頭指標,還需要知道每個變數的偏移位置和大小才可以修改記憶體
let rawPtr = UnsafeMutableRawPointer(pigPtr) //轉換指標 型別同void *
let aPtr = rawPtr.advanced(by: 0).assumingMemoryBound(to: Int.self) //advanced函式時位元組偏移,assumingMemoryBound是記憶體大小
print("修改前:\(aPtr.pointee)") //4
pig.printA() //count等於4
aPtr.initialize(to: 100) //將count引數修改為100,即篡改了私有成員
print("修改後:\(aPtr.pointee)") //100
pig.printA()
輸出:
修改前:4
Animal a:4
修改後:100
Animal a:100
類是引用型別, 例項是在Heap堆區域裡, 而Stack棧裡只是存放了指向它的指標; 而Swift是ARC即自動回收的,類相比於結構體需要額外的記憶體空間用於存放型別資訊和引用計數。 在32bit機型上型別資訊佔4個位元組,在64bit機型上型別資訊佔8位元組; 引用計數佔8位元組。 考慮到記憶體對齊原因, 類屬性總是從16位元組開始。
要修改類成員變數, 跟上面介紹的結構體類似, 但成員起始位置是第16個位元組, 因為型別資訊和引用計數佔用的記憶體空間在類成員屬性前面。 將Pig修改為類,對比一下:
//在iPhone7上測試
class Pig {
private var count = 4 //8位元組
var name = "Tom" //24位元組
// 得到Pig物件在堆記憶體的首位置
func headPointerOfClass() -> UnsafeMutablePointer<Int8> {
let opaquePointer = Unmanaged.passUnretained(self as AnyObject).toOpaque()
let mutableTypedPointer = opaquePointer.bindMemory(to: Int8.self, capacity: MemoryLayout<Pig>.stride)
return UnsafeMutablePointer<Int8>(mutableTypedPointer)
}
func printA() {
print("Animal a:\(count)")
}
}
var pig = Pig()
let pigPtr: UnsafeMutablePointer<Int8> = pig.headPointerOfClass() //頭指標, 型別同const void *
//有了頭指標,還需要知道每個變數的偏移位置和大小才可以修改記憶體
let rawPtr = UnsafeMutableRawPointer(pigPtr) //轉換指標 型別同void *
//在iPhone7上型別資訊和引用計數引數佔用16個位元組,類成員屬性相對起始位置要偏移16個位元組
let aPtr = rawPtr.advanced(by: 16).assumingMemoryBound(to: Int.self) //advanced函式時位元組偏移,assumingMemoryBound是記憶體大小
print("修改前:\(aPtr.pointee)") //4
pig.printA() //count等於4
aPtr.initialize(to: 100) //將count引數修改為100,即篡改了私有成員
print("修改後:\(aPtr.pointee)") //100
pig.printA()
輸出:
修改前:4Animal a:4
修改後:100
Animal a:100
結構體和類物件在篡改記憶體資料時, 結構體成員引數起始偏移為0, 類成員引數起始偏移為型別資訊和引用計數佔用空間之和。
直接操作記憶體是高階玩法, 要判斷當前機型CPU的位數(即需要適配),然後要理解記憶體模型。
出個小題考驗一下你是否理解了Swift記憶體模型:
struct Point {
var a: Double?
var b = 0
}
從記憶體角度考慮, Point結構體有什麼問題?如果你沒懵逼, 那麼恭喜你已經掌握了Swift記憶體模型原理。 老司機應該這樣寫:
struct Point {
var b = 0
var a: Double?
}
理由:因為Optional會多佔1個位元組, 第一種寫法後面的Int型引數b會先記憶體對齊,然後再分配記憶體,即多佔了一個Int型空間(PS:要減1)。
參考: