本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:https://mp.weixin.qq.com/s/zIkB9KnAt1YPWGOOwyqY3Q
作者:王振宇
HandyJSON
是Swift
处理JSON
数据的开源库之一,类似JOSNModel
,它可以直接将JSON
数据转化为类实例在代码中使用。由于
Swift
是一种静态语言,没有OC
那种灵活的Runtime
机制,为了达到类似JSONModel
的效果,HandyJSON
另辟蹊径,绕过对Runtime
的依赖,直接操作实例的内存对实例属性进行赋值,从而得到一个完全初始化完成的实例。本文将通过探究 Swift 对象内存模型机制,简单介绍
HandyJSON
实现原理.
内存分配
- Stack(栈),存储值类型的临时变量,函数调用栈,引用类型的临时变量指针
- Heap(堆),存储引用类型的实例
MemoryLayout
基本使用方法
MemoryLayout
是 Swift3.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\
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 *
- unsafeMutablePointer
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)
方法我们拿到了指向属性 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的数组使用
更多精彩内容欢迎关注腾讯 Bugly的微信公众账号:
腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的情况以及解决方案。智能合并功能帮助开发同学把每天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同学定位到出问题的代码行,实时上报可以在发布后快速的了解应用的质量情况,适配最新的 iOS, Android 官方操作系统,鹅厂的工程师都在使用,快来加入我们吧!