Swift 對象內存模型探究(一)


本文來自於騰訊Bugly公眾號(weixinBugly),未經作者同意,請勿轉載,原文地址:https://mp.weixin.qq.com/s/zIkB9KnAt1YPWGOOwyqY3Q

作者:王振宇

HandyJSONSwift 處理 JSON 數據的開源庫之一,類似 JOSNModel,它可以直接將 JSON 數據轉化為類實例在代碼中使用。

由於 Swift 是一種靜態語言,沒有 OC 那種靈活的 Runtime 機制,為了達到類似 JSONModel 的效果,HandyJSON 另辟蹊徑,繞過對 Runtime 的依賴,直接操作實例的內存對實例屬性進行賦值,從而得到一個完全初始化完成的實例。

本文將通過探究 Swift 對象內存模型機制,簡單介紹 HandyJSON 實現原理.

內存分配

  • Stack(棧),存儲值類型的臨時變量,函數調用棧,引用類型的臨時變量指針
  • Heap(堆),存儲引用類型的實例

MemoryLayout

基本使用方法

MemoryLayoutSwift3.0 推出的一個工具類,用來計算數據占用內存的大小。基本的用法如下:

MemoryLayout<Int>.size   //8

let a: Int = 10
MemoryLayout.size(ofValue: a)   //8

MemoryLayout 屬性介紹

MemoryLayout 有三個非常有用的屬性,都是 Int 類型:

alignment & alignment(ofValue: T)

這個屬性是與內存對齊相關的屬性。許多計算機系統對基本數據類型的合法地址做出了一些限制,要求某種數據類型對象的地址必須是某個值 K(通常是 2、4或者8)的倍數。這種對齊限制簡化了形成處理器和內存系統之間接口的硬件設計。對齊原則是任何 K 字節的基本對象的地址必須是 K 的倍數。

MemoryLayout<T>.alignment 就代表着數據類型 T 的內存對齊原則。而且在 64bit 系統下,最大的內存對齊原則是 8byte。

size & size(ofValue: T)

一個 T 數據類型實例占用連續內存字節的大小。

stride & stride(ofValue: T)

在一個 T 類型的數組中,其中任意一個元素從開始地址到結束地址所占用的連續內存字節的大小就是 stride。 如圖:

注釋:數組中有四個 T 類型元素,雖然每個 T 元素的大小為 size 個字節,但是因為需要內存對齊的限制,每個 T 類型元素實際消耗的內存空間為 stride 個字節,而 stride - size 個字節則為每個元素因為內存對齊而浪費的內存空間。

基本數據類型的 MemoryLayout

//值類型
MemoryLayout<Int>.size           //8
MemoryLayout<Int>.alignment      //8
MemoryLayout<Int>.stride         //8

MemoryLayout<String>.size           //24
MemoryLayout<String>.alignment      //8
MemoryLayout<String>.stride         //24

//引用類型 T
MemoryLayout<T>.size           //8
MemoryLayout<T>.alignment      //8
MemoryLayout<T>.stride         //8


//指針類型
MemoryLayout<unsafeMutablePointer<T>>.size           //8
MemoryLayout<unsafeMutablePointer<T>>.alignment      //8
MemoryLayout<unsafeMutablePointer<T>>.stride         //8

MemoryLayout<unsafeMutableBufferPointer<T>>.size           //16
MemoryLayout<unsafeMutableBufferPointer<T>>.alignment      //16
MemoryLayout<unsafeMutableBufferPointer<T>>.stride         //16

Swift 指針

常用 Swift 指針類型

在本文中主要涉及到幾種指針的使用,在此簡單類比介紹一下。

  • unsafePointer
    unsafePointer<T> 等同於 const T *.
  • unsafeMutablePointer
    unsafeMutablePointer<T> 等同於 T *
  • unsafeRawPointer
    unsafeRawPointer 等同於 const void *
  • unsafeMutableRawPointer
    unsafeMutableRawPointer 等同於 void *

Swift 獲取指向對象的指針

final func withUnsafeMutablePointers<R>(_ body: (UnsafeMutablePointer<Header>, UnsafeMutablePointer<Element>) throws -> R) rethrows -> R

//基本數據類型
var a: T = T()
var aPointer = a.withUnsafeMutablePointer{ return $0 }

//獲取 struct 類型實例的指針,From HandyJSON
func headPointerOfStruct() -> UnsafeMutablePointer<Int8> {
    return withUnsafeMutablePointer(to: &self) {
        return UnsafeMutableRawPointer($0).bindMemory(to: Int8.self, capacity: MemoryLayout<Self>.stride)
     }
}

//獲取 class 類型實例的指針,From HandyJSON
func headPointerOfClass() -> UnsafeMutablePointer<Int8> {
    let opaquePointer = Unmanaged.passUnretained(self as AnyObject).toOpaque()
    let mutableTypedPointer = opaquePointer.bindMemory(to: Int8.self, capacity: MemoryLayout<Self>.stride)
    return UnsafeMutablePointer<Int8>(mutableTypedPointer)
}

Struct 內存模型

在 Swift 中,struct 是值類型,一個沒有引用類型的 Struct 臨時變量都是在棧上存儲的:

struct Point {
    var a: Double
    var b: Double
}

MemoryLayout<Point>.size     //16

內存模型如圖:

再看另一種情況:

struct Point {
    var a: Double?
    var b: Double
}

MemoryLayout<Point>.size    //24

可以看到,如果將屬性 a 變成可選類型,整個 Point 類型增加了 8 個字節。但是實際上,可選類型只增加一個字節:

MemoryLayout<Double>.size               //8
MemoryLayout<Optional<Double>>.size     //9

之所以 a 屬性為可選值后 Point 類型增加了 8 個字節的存儲空間,還是因為內存對齊限制搞的鬼:

由於 Optional<Double> 占用了前 9 個字節,導致第二個格子剩下 7 個字節,而屬性 b 為 Double 類型 alignment 為 8,所以 b 屬性的存儲只能從第 16 個字節開始,從而導致整個 Point 類型的存儲空間變為 24byte,其中 7 個字節是被浪費掉的。

所以,從以上例子可以得出一個結論:Swift 的可選類型是非常浪費內存空間的。

操作內存修改一個 Struct 類型實例的屬性的值

struct Demo

下面展示了一個簡單的結構體,我們將用這個結構體來完成一個示例操作:

enum Kind {
    case wolf
    case fox
    case dog
    case sheep
}

struct Animal {
    private var a: Int = 1       //8 byte
    var b: String = "animal"     //24 byte
    var c: Kind = .wolf          //1 byte
    var d: String?               //25 byte
    var e: Int8 = 8              //1 byte
    
    //返回指向 Animal 實例頭部的指針
    func headPointerOfStruct() -> UnsafeMutablePointer<Int8> {
        return withUnsafeMutablePointer(to: &self) {
            return UnsafeMutableRawPointer($0).bindMemory(to: Int8.self, capacity: MemoryLayout<Self>.stride)
     }
    
    func printA() {
        print("Animal a:\(a)")
    }
}

操作

首選我們需要初始化一個 Animal 實例:

let animal = Animal()     // a: 1, b: "animal", c: .wolf, d: nil, e: 8 

拿到指向 animal 的指針:

let animalPtr: unsafeMutablePointer<Int8> = animal.headPointerOfStruct()

現在內存中的情況如圖所示:

PS: 由圖可以看到 Animal 類型的 size 為 8 + 24 + 8 + 25 + 1 = 66, alginment 為 8, stride 為 8 + 24 + 8 + 32 = 72.

如果我們想要通過內存修改 animal 實例的屬性值,那么就需要獲取到它的屬性值所在的內存區域,然后修改內存區域的值,就可以達到修改 animal 屬性值的目的了:

//將之前得到的指向 animal 實例的指針轉化為 rawPointer 指針類型,方便我們進行指針偏移操作
let animalRawPtr = unsafeMutableRawPointer(animalPtr)
let intValueFromJson = 100

let aPtr = animalRawPtr.advance(by: 0).assumingMemoryBound(to: Int.self)
aPtr.pointee          // 1
animal.printA()       //Animal a: 1
aPtr.initialize(to: intValueFromJson)
aPtr.pointee          // 100
animal.printA()       //Animal a:100

通過以上操作,我們成功把 animal 的一個 Int 類型屬性的值由 1 修改成了 100,而且這個屬性還是一個私有屬性。

代碼分析

首先,animalPtr 指針是一個 Int8 類型的指針,也可以說是 byte 類型的指針,它表示 animal 實例所在內存的第一個字節。而想要獲取到 animal 實例的屬性 a, 需要一個 Int 類型的指針,顯然 animalPtr 作為一個 Int8 類型的指針是不符合要求的。

所以,我們先將 animalPtr 轉換為 unsafeMutableRawPointer 類型(相當於 C 中的 void * 類型)。因為屬性 a 在內存中的偏移為 0,偏移 0 個字節。然后通過 assumingMemoryBound(to: Type) 方法來得到一個指向地址相同但是類型為指定類型 Type(在此例中為 Int) 的指針。於是,我們得到了一個指向 animal 實例首地址但是類型為 Int 類型的指針。

assumingMemoryBound(to:) 方法在文檔中是這樣說明的:

Returns a typed pointer to the memory referenced by this pointer, assuming that the memory is already bound to the specified type

默認某塊內存區域已經綁定了某種數據類型(在本例中如圖綠色的內存區域是 Int 類型,所以我們就可以默認此塊區域為 Int 類型),返回一個指向此塊內存區域的此種數據類型指針(在本例中,我們將 Int.self 作為類型參數傳入,並返回了一個指向綠色內存區域的 Int 類型的指針)。

所以,通過 assumingMemoryBound(to: Int.self) 方法我們拿到了指向屬性 aInt 類型指針 aPtr

在 Swift 中指針有一個叫做 pointee 的屬性,我們可以通過這個屬性拿到指針指向的內存中的值,類似 C 中的 *Pointer 來拿到指針的值。

因為 animal 實例初始化的時候 a 的默認值為 1,所以此時 aPtr.pointee 的值也是 1.

之后,我們使用 initialize(to:) 方法來重新初始化 aPtr 指向的內存區域,也就是途中的綠色的區域,將其值改為 100. 這樣,通過內存來修改屬性 a 的值的操作就完成了。

修改后面屬性值的思路都是一樣的,首先通過對 animalRawPtr 進行指針偏移得到一個指向某屬性開始地址的指針,然后對此塊內存區域通過 assumingMemoryBound(to:) 方法進行指針類型轉換,然后轉換好的指針通過重新初始化此塊內存區域的方式重寫這塊內存區域的值,完成修改操作。

Class 內存模型

class 是引用類型,生成的實例分布在 Heap(堆) 內存區域上,在 Stack(棧)只存放着一個指向堆中實例的指針。因為考慮到引用類型的動態性和 ARC 的原因,class 類型實例需要有一塊單獨區域存儲類型信息和引用計數。

class Human {
    var age: Int?
    var name: String?
    var nicknames: [String] = [String]()
    
    //返回指向 Human 實例頭部的指針
    func headPointerOfClass() -> UnsafeMutablePointer<Int8> {
        let opaquePointer = Unmanaged.passUnretained(self as AnyObject).toOpaque()
        let mutableTypedPointer = opaquePointer.bindMemory(to: Int8.self, capacity: MemoryLayout<Human>.stride)
        return UnsafeMutablePointer<Int8>(mutableTypedPointer)
    }
}

MemoryLayout<Human>.size       //8

Human 類內存分布如圖:

類型信息區域在 32bit 的機子上是 4byte,在 64bit 機子上是 8 byte。引用計數占用 8 byte。所以,在堆上,類屬性的地址是從第 16 個字節開始的。

操作內存修改一個 Class 類型實例屬性的值

與修改 struct 類型屬性的值一樣, 唯一點區別是,拿到 class 實例堆上的首地址后,因為 Type 字段和引用計數字段的存在,需要偏移 16 個字節才達到第一個屬性的內存起始地址。下面這個例子介紹了修改 nicknames 屬性的操作:

let human = Human()
let arrFormJson = ["goudan","zhaosi", "wangwu"]

//拿到指向 human 堆內存的 void * 指針
let humanRawPtr = unsafeMutableRawPointer(human.headerPointerOfClass())

//nicknames 數組在內存中偏移 64byte 的位置(16 + 16 + 32)
let humanNickNamesPtr =  humanRawPtr.advance(by: 64).assumingMemoryBound(to: Array<String>.self)
human.nicknames      
     //[]
humanNickNamePtr.initialize(arrFormJson)
human.nicknames           //["goudan","zhaosi", "wangwu"]

玩一玩 Class 類型中的數組屬性

Human 類型內存示意圖所示,human 實例持有 nicknames 數組其實只是持有了一個 Array<String> 類型的指針,就是圖中的 nicknames 區域。真正的數組在堆中另外一塊連續的內存中。下面就介紹一下怎么拿到那塊真正存放數組數據的連續內存區域。

在 C 中,指向數組的指針其實是指向數組中的第一個元素的,比如假設 arrPointer 是 C 中一個指向數組的指針,那么我們就可以通過 *arrPointer 這種操作就可以獲取到數組的第一個元素,也就是說, arrPointer 指針指向的是數組的第一個元素,而且指針的類型和數組的元素類型是相同的。

同理,在 Swift 中也是適用的。在本例中,nicknames 內存區域包含的指針指向的是一個 String 類型的數組,也就是說,此指針指向的是 String 類型數組的第一個元素。所以,這個指針的類型應該是 unsafeMuatblePointer<String>, 所以,我們可以通過以下方式拿到指向數組的指針:

let firstElementPtr = humanRawPtr.advance(by: 64).assumingMemoryBound(to: unsafeMutablePointer<String>.self).pointee 

如圖:

所以,在理論上,我么就可以用 firstElementPtrpointee 屬性來取得數組的第一個元素 “goudan” 了,看代碼:

在 Playground 上運行后並沒有像我們的預期一樣顯示出 “goudan”,難道我們的理論不對嗎,這不科學!本着打破砂鍋問到底,問題解決不了就睡不着覺的精神,果然摸索出了一點規律:

通過直接獲取到原數組 arrFormJson 的地址與 firstElementPtr 對比我們發現,通過我們的方式獲取到的 firstElementPtr 指向的地址總是比原數組 arrFromJson 的真實地址低 32byte(經過博主的多輪測試,無論什么類型的數組,兩種方式獲取到的地址總是差 32 個字節)。

可以看到,0x6080000CE870 0x6080000CE850 差了 0x20 個字節也就是十進制的 32 個字節。

所以,通過我們的方式獲取到的 firstElementPtr 指針指向的真實地址是這樣的,如圖:

PS: 雖然原因搞明白了,但是數組開頭的那 32 個字節博主至今沒搞明白是做啥用的,有了解的童鞋可以告知一下博主。

所以,我們需要做的就是將 firstElementPtr 偏移 32 個字節,然后再取值就可以拿到數組中的值了。

Class Type 之掛羊頭賣狗肉

Type 的作用

先假設如下代碼:

class Drawable {
    func draw() {
        
    }
}

class Point: Drawable {
    var x: Double = 1
    var y: Double = 1
    
    func draw() {
        print("Point")
    }
}

class Line: Drawable {
    var x1: Double = 1
    var y1: Double = 1
    var x2: Double = 2
    var y2: Double = 2 
    
    func draw() {
        print("Line")
    }
}

var arr: [Drawable] = [Point(), Line()]
for d in arr {
    d.draw()     //問題來了,Swift 是如何判斷該調用哪一個方法的呢?
}

在 Swift 中,class 類型的方法派發是通過 V-Table 來實現動態派發的。Swift 會為每一種類類型生成一個 Type 信息並放在靜態內存區域中,而每個類類型實例的 type 指針就指向靜態內存區域中本類型的 Type 信息。當某個類實例調用方法的時候,首先會通過該實例的 type 指針找到該類型的 Type 信息,然后通過信息中的 V-Table 得到方法的地址,並跳轉到相應的方法的實現地址去執行方法。

替換一下 Type 會怎樣

通過上面的分析,我們知道一個類類型的方法派發是通過頭部的 type 指針來決定的,如果我們將某個類實例的 type 指針指向另一個 type 會不會有什么好玩的事情發生呢?哈哈 ~ 一起來試試 ~

class Wolf {
    var name: String = "wolf"
    
    func soul() {
        print("my soul is wolf")
    }
    
    func headPointerOfClass() -> UnsafeMutablePointer<Int8> {
        let opaquePointer = Unmanaged.passUnretained(self as AnyObject).toOpaque()
        let mutableTypedPointer = opaquePointer.bindMemory(to: Int8.self, capacity: MemoryLayout<Wolf>.stride)
        return UnsafeMutablePointer<Int8>(mutableTypedPointer)
    }
}

class Fox {
    var name: String = "fox"
    
    func soul() {
        print("my soul is fox")
    }
    
    func headPointerOfClass() -> UnsafeMutablePointer<Int8> {
        let opaquePointer = Unmanaged.passUnretained(self as AnyObject).toOpaque()
        let mutableTypedPointer = opaquePointer.bindMemory(to: Int8.self, capacity: MemoryLayout<Fox>.stride)
        return UnsafeMutablePointer<Int8>(mutableTypedPointer)
    }
}

可以看到以上 WolfFox 兩個類除了 Type 不一樣之外,兩個類的內存結構是一模一樣的。那我們就可以用這兩個類來做測試:

let wolf = Wolf()
var wolfPtr = UnsafeMutableRawPointer(wolf.headPointerOfClass())

let fox = Fox()
var foxPtr = UnsafeMutableRawPointer(fox.headPointerOfClass())
foxPtr.advanced(by: 0).bindMemory(to: UnsafeMutablePointer<Wolf.Type>.self, capacity: 1).initialize(to: wolfPtr.advanced(by: 0).assumingMemoryBound(to: UnsafeMutablePointer<Wolf.Type>.self).pointee)

print(type(of: fox))        //Wolf
fox.name                    //"fox"
fox.soul()                  //my soul is wolf

神奇的事情發生了,一個 Fox 類型的實例竟然調用了 Wolf 類型的方法,哈哈 ~ 如果還有什么好玩的玩法,大家可以繼續探究 ~

參考文章

Swift進階之內存模型和方法調度
Swift 中的指針使用
從Swift看Objective-C的數組使用


更多精彩內容歡迎關注騰訊 Bugly的微信公眾賬號:

騰訊 Bugly是一款專為移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的情況以及解決方案。智能合並功能幫助開發同學把每天上報的數千條 Crash 根據根因合並分類,每日日報會列出影響用戶數最多的崩潰,精准定位功能幫助開發同學定位到出問題的代碼行,實時上報可以在發布后快速的了解應用的質量情況,適配最新的 iOS, Android 官方操作系統,鵝廠的工程師都在使用,快來加入我們吧!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM