標籤:bic code 假設 rpo unsafe btn 十進位 重寫 out
MemoryLayout基本使用方法
HandyJSON
是 Swift
處理 JSON
資料的開源庫之一,類似 JOSNModel
,它可以直接將 JSON
資料轉化為類執行個體在代碼中使用。
由於 Swift
是一種靜態語言,沒有 OC
那種靈活的 Runtime
機制,為了達到類似 JSONModel
的效果,HandyJSON
另闢蹊徑,繞過對 Runtime
的依賴,直接操作執行個體的記憶體對執行個體屬性進行賦值,從而得到一個完全初始化完成的執行個體。
本文將通過探究 Swift 對象記憶體模型機制,簡單介紹 HandyJSON
實現原理.
記憶體配置
MemoryLayout
是 Swift3.0
推出的一個工具類,用來計算資料佔用記憶體的大小。基本的用法如下:
MemoryLayout<Int>.size //8
let a: Int = 10
MemoryLayout.size(ofValue: a) //8
MemoryLayout 屬性介紹
MemoryLayout
有三個非常有用的屬性,都是 Int
類型:
alignment
& alignment(ofValue: T)
這個屬性是與記憶體對齊相關的屬性。許多電腦系統對基礎資料型別 (Elementary Data Type)的合法地址做出了一些限制,要求某種資料類型對象的地址必須是某個值 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
個位元組則為每個元素因為記憶體對齊而浪費的記憶體空間。
基礎資料型別 (Elementary Data Type)的 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 *
.
Swift 擷取指向對象的指標
final func withUnsafeMutablePointers<R>(_ body: (UnsafeMutablePointer<Header>, UnsafeMutablePointer<Element>) throws -> R) rethrows -> R
//基礎資料型別 (Elementary Data Type)
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)
方法我們拿到了指向屬性 a
的 Int
類型指標 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
所以,在理論上,我麼就可以用 firstElementPtr
的 pointee
屬性來取得數組的第一個元素 “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) }}
可以看到以上 Wolf
和 Fox
兩個類除了 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的數組使用
https://mp.weixin.qq.com/s/zIkB9KnAt1YPWGOOwyqY3Q
Swift 對象記憶體模型探究(一)