跟OC類似, Swift提供了MemoryLayout類靜態(tài)測量對象大小, 注意是在編譯時確定的,不是運行時哦! 作為Java程序員想想如何測量Java對象大小? 參考 Java對象到底有多大?
寫這篇博客的目的在于說明2個黑科技:
1、 類/結構體的成員變量聲明順序會影響它的占用空間。 原理是內存對齊, 有經驗的碼農會把占用空間大的變量寫在前面, 占用空間小的寫在后面。 PS: 大道同源,C/C++/Object-C/swift/Java都需要字節(jié)對齊; 如果面試時問你內存優(yōu)化有什么經驗? 你告訴他這個一定會另眼相看!!!
2、 可以篡改Swift結構體/類對象的私有成員(通過指針操作內存)。 Java要用反射實現。
** **為什么要內存對齊呢?簡單來說就是CPU尋址更快,詳情參見 內存對齊原因
** ** 在不同機型上數據類型占用的空間大小也不同, 例如iPhone5上Int占4個字節(jié), iPhone7上Int占8個字節(jié)。 本文是在iPhone7模擬器上驗證的。
內存分配:參考 結構體和類的區(qū)別
Stack(棧),存儲值類型的臨時變量,函數調用棧,引用類型的臨時變量指針,結構體對象和類對象引用
Heap(堆),存儲引用類型的實例,例如類對象
Swift3.0提供了內存操作類MemoryLayout(注意:Swift跟OC一樣,內存排列時需要對齊,造成一定的內存浪費,我們稱之為內存碎片), 它有3個主要參數:
1、實例方法alignment和靜態(tài)方法 alignment(ofValue: T):
字節(jié)對齊屬性,它要求當前數據類型相對于起始位置的偏移必須是alignment的整數倍。 例如在iPhone7上Int占8個字節(jié),那么在類/結構體中Int型參數的起始位置必須是8的整數倍(可認為類/結構體第一個成員變量的內存起始位置為0), 后面會用實例說明。
2、 實例成員變量size和靜態(tài)方法size(ofValue: T)
得到一個 T 數據類型實例占用連續(xù)內存字節(jié)的大小。
3、實例成員變量stride和靜態(tài)方法 stride(ofValue: T)
在一個 T 類型的數組中,其中任意一個元素從開始地址到結束地址所占用的連續(xù)內存字節(jié)的大小就是 stride。 如圖:
注釋:數組中有四個 T 類型元素,雖然每個 T 元素的大小為 size 個字節(jié),但是因為需要內存對齊的限制,每個 T 類型元素實際消耗的內存空間為 stride 個字節(jié),而 stride - size 個字節(jié)則為每個元素因為內存對齊而浪費的內存空間。
所以, 一個對象或變量占用的空間是由本身大小和偏移組成的! 我們改變不了數據類型本身的大小, 但我們可以盡量的縮小偏移, 后面會講怎么做!
下面用幾個實例說明:
[java] view plain copy
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占用內存大小相同, 區(qū)別是指向的內存空間占用大小不同!
如果將類改成結構體會怎樣?
[java] view plain copy
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個字節(jié),String占24個字節(jié); 所以people1占用24個字節(jié),people2占用32個字節(jié)。
類/結構體的成員聲明順序會影響占用空間,原理是變量要以自身類型的aligment整數倍作為起始地址,不足的話要在前面補齊字節(jié)(即內存碎片)。 下面示例說明: 結構體People1和People2的參數相同,區(qū)別是先后順序不一致。
[java] view plain copy
//在iPhone7上占16個字節(jié)
struct People2 {
var enable = false //占用1個字節(jié)
var age = 1 //Int占8個字節(jié), 因為是8字節(jié)對齊,必須以8的整數倍開始,所以前面要補齊7個字節(jié)偏移。
}
//在iPhone7上占9個字節(jié)
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=16People3: size=9 align=8 stride=16
Optional即可選數據類型會增加1個字節(jié)空間, 由于內存對齊的原因,Optional可能占用更多的內存空間。下面以Int為例:
[java] view plain copy
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。
空類和空結構體占多大空間呢?
[java] view plain copy
class EmptyClass {
//占用一個引用的大小
}
struct EmptyStruct {
//占1個字節(jié),因為需要唯一的地址
}
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個字節(jié)(跟引用占用空間大小相等), 即使在EmptyClass添加幾個成員變量, 得到的size仍然是8, 其實這里實際上測量的是引用; EmptyStruct的size為0但stride為1, 說明占用1個字節(jié)空間,因為每個實例都需要唯一的地址。
如果你想知道類到底占用多大內存, 那么你可以嘗試改為結構體后測量一下! 因為你測量的是類的引用。
相信你對Swift內存占用情況有了一定的理解, 現在說說如何篡改內存。Swift提供了UnSafePointer類操作指針( 還記得Java怎樣操作指針嗎?看我前面的博客), [iOS](http://lib.csdn.net/base/ios)不負責UnSafePointer的回收, 所有在使用它時要注意回收內存。下面是Swift的所有指針操作類:
如果你想操作類/結構體的內存,可以繼承于_PropertiesMetriczable或對應函數。 下面代碼摘自HandyJSON:
[java] view plain copy
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)
}
}
使用結構體親測一下篡改私有變量(原理: 拿到對象頭指針、判斷出成員變量的偏移和占用內存大小,然后寫內存):
[java] view plain copy
//在iPhone7上測試
struct Pig {
private var count = 4 //8字節(jié)
var name = "Tom" //24字節(jié)
//返回指向 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函數時字節(jié)偏移,assumingMemoryBound是內存大小
print("修改前:\(aPtr.pointee)") //4
pig.printA() //count等于4
aPtr.initialize(to: 100) //將count參數修改為100,即篡改了私有成員
print("修改后:\(aPtr.pointee)") //100
pig.printA()
輸出:
修改前:4Animal a:4修改后:100Animal a:100
類是引用類型, 實例是在Heap堆區(qū)域里, 而Stack棧里只是存放了指向它的指針; 而Swift是ARC即自動回收的,類相比于結構體需要額外的內存空間用于存放類型信息和引用計數。 在32bit機型上類型信息占4個字節(jié),在64bit機型上類型信息站8字節(jié); 引用計數占8字節(jié)。 考慮到內存對齊原因, 類屬性總是從16字節(jié)開始。
要修改類成員變量, 跟上面介紹的結構體類似, 但成員起始位置是第16個字節(jié), 因為類型信息和引用計數占用的內存空間在類成員屬性前面。 將Pig修改為類,對比一下:
[java] view plain copy
//在iPhone7上測試
class Pig {
private var count = 4 //8字節(jié)
var name = "Tom" //24字節(jié)
// 得到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個字節(jié),類成員屬性相對起始位置要偏移16個字節(jié)
let aPtr = rawPtr.advanced(by: 16).assumingMemoryBound(to: Int.self) //advanced函數時字節(jié)偏移,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
**結構體和類對象在篡改內存數據時, 結構體成員參數起始偏移為0, 類成員參數起始偏移為類型信息和引用計數占用空間之和。 **
直接操作內存是高階玩法, 要判斷當前機型CPU的位數(即需要適配),然后要理解內存模型。
出個小題考驗一下你是否理解了Swift內存模型:
[java] view plain copy
struct Point {
var a: Double?
var b = 0
}
從內存角度考慮, Point結構體有什么問題?
如果你沒懵逼, 那么恭喜你已經掌握了Swift內存模型原理。 老司機應該這樣寫:
[java] view plain copy
struct Point {
var b = 0
var a: Double?
}
理由:因為Optional會多占1個字節(jié), 第一種寫法后面的Int型參數b會先內存對齊,然后再分配內存,即多占了一個Int型空間(PS:要減1)。