Swift 对象内存模型探究(一)

4,313 阅读14分钟

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\.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的数组使用



如果您觉得我们的内容还不错,就请转发到朋友圈,和小伙伴一起分享吧~