天天看點

Swift記憶體模型的那點事兒

        跟OC類似, Swift提供了MemoryLayout類靜态測量對象大小, 注意是在編譯時确定的,不是運作時哦! 作為Java程式員想想如何測量Java對象大小? 參考 Java對象到底有多大?

       寫這篇部落格的目的在于說明2個黑科技:

1、 類/結構體的成員變量聲明順序會影響它的占用空間。 原理是記憶體對齊, 有經驗的碼農會把占用空間大的變量寫在前面, 占用空間小的寫在後面。 PS: 大道同源,C/C++/Object-C/Swift/Java都需要位元組對齊; 如果面試時問你記憶體優化有什麼經驗? 你告訴他這個一定會另眼相看!!!

2、 可以篡改Swift結構體/類對象的私有成員(通過指針操作記憶體)。 Java要用反射實作。

       為什麼要記憶體對齊呢?簡單來說就是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。 如圖:

Swift記憶體模型的那點事兒

注釋:數組中有四個 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=8

Optional 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=8

EmptyStruct:  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的所有指針操作類:

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()
           

輸出:

修改前:4

Animal 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)。

參考:

Swift 對象記憶體模型探究(一)

Swift進階之記憶體模型和方法排程

Swift操作指針詳解