Swift 对象内存模型探究(一)
本文来自于腾讯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<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)
方法我们拿到了指向属性 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 官方操作系统,鹅厂的工程师都在使用,快来加入我们吧!
Swift 对象内存模型探究(一)的更多相关文章
- C++/C#中堆栈、对象内存模型、深浅拷贝、Array.Clone方法
转载自:http://blog.csdn.net/jarvischu/article/details/6425534 目录 1. C++/C#中对象内存模型................. ...
- C#的对象内存模型
转载自:http://www.cnblogs.com/alana/archive/2012/07/05/2577893.html C#的对象内存模型: 一.栈内存和堆内存1.栈内存 由编译器自动分配和 ...
- C++对象内存模型2 (虚函数,虚指针,虚函数表)
从例子入手,考察如下带有虚函数的类的对象内存模型: class A { public: virtual void vfunc1(); virtual void vfunc2(); void func1 ...
- C++对象内存模型1(堆栈模型)
对象内存模型 一. 栈(Stack) VS. 堆(heap) 栈 由系统自动管理,以执行函数为单位 空间大小编译时确定(参数+局部变量) 函数执行时,系统自动分配一个stack 函数执行结束时,系统立 ...
- (转)c#对象内存模型
对象内存模型 C#的对象内存模型写这篇博客的主要目的是为了加深自己的理解,如有不对的地方,请各位见谅. C#的对象内存模型: 一.栈内存和堆内存1.栈内存 由编译器自动分配和释放,主要用来保存一些局部 ...
- 从零开始学C++之虚继承和虚函数对C++对象内存模型造成的影响
首先重新回顾一下关于类/对象大小的计算原则: 类大小计算遵循结构体对齐原则 第一个数据成员放在offset为0的位置 其它成员对齐至min(sizeof(member),#pragma pack(n) ...
- 对C++对象内存模型造成的影响(类/对象的大小)
首先重新回顾一下关于类/对象大小的计算原则: 类大小计算遵循结构体对齐原则 第一个数据成员放在offset为0的位置 其它成员对齐至min(sizeof(member),#pragma pack(n) ...
- Objective-C类成员变量深度剖析--oc对象内存模型
目录 Non Fragile ivars 为什么Non Fragile ivars很关键 如何寻址类成员变量 真正的“如何寻址类成员变量” Non Fragile ivars布局调整 为什么Objec ...
- C++对象内存模型1(堆栈模型)(转)
对象内存模型 一. 栈(Stack) VS. 堆(heap) 栈 由系统自动管理,以执行函数为单位 空间大小编译时确定(参数+局部变量) 函数执行时,系统自动分配一个stack 函数执行结束时,系统立 ...
随机推荐
- 初学python之路-day06
每天一篇总结,今天学习了大概有深浅拷贝,元组类型,字典类型与集合类型.第一次感觉有点难度,需要花费多点时间来掌握. 深浅拷贝,分为值拷贝.浅拷贝.深拷贝. ls = [1, 'abc', [10]] ...
- 最优的路线(floyd最小环)
问题描述 学校里面有N个景点.两个景点之间可能直接有道路相连,用Dist[I,J]表示它的长度:否则它们之间没有直接的道路相连.这里所说的道路是没有规定方向的,也就是说,如果从I到J有直接的道路,那么 ...
- git在项目中的实际运用
项目中只运用git版本管理的情况下: 1.创建分支命令: git branch (branchname) 切换分支命令: git checkout (branchname) 当你切换分支的时候,Git ...
- 饮冰三年-人工智能-Python-30 python开发中常见的错误
1:触发条件:创建的实体类生成到数据库表时报错 报错信息:TypeError: __init__() missing 1 required positional argument: 'on_delet ...
- 2018-2019-2 网络对抗技术 20165328 Exp1 PC平台逆向破解
实验目的: 本次实践的对象是一个名为pwn1的linux可执行文件. 该程序正常执行流程是:main调用foo函数,foo函数会简单回显任何用户输入的字符串. 该程序同时包含另一个代码片段,getSh ...
- 解决MongoDB登录的WARNING
解决MongoDB登录的WARNING 1.安装完成运行mongodb警告内容如下: 2019-01-17T15:41:50.578+0800 I CONTROL [initandlisten] ** ...
- C#学习-显式接口
显式的接口实现解决了命名冲突问题. 在使用显式的接口实现方式时,需要注意以下几个问题. 若显式实现接口,方法不能使用任何访问修饰符,显式实现的成员都默认为私有: 现式实现的成员默认是私有的,所以这些成 ...
- 函数式编程-compose与pipe
函数式编程中有一种模式是通过组合多个函数的功能来实现一个组合函数.一般支持函数式编程的工具库都实现了这种模式,这种模式一般被称作compose与pipe.以函数式著称的Ramda工具库为例. cons ...
- About The Order of The Declarations And Definition When Making a Member Function a Friend.关于使类成员成为另一个类友元函数的声明顺序和定义。
If only member function clear of WindowMgr is a friend of Screen, there are some points need to note ...
- 【Vue】删除数组元素,导致剩余元素被重新渲染
最近在项目中有使用Vue,然而在开发过程中发现,当我对数组中的元素进行删除时,会导致该元素后面的元素没有被重新渲染. html代码如下:有两个组件:一个是Main组件,用来包含所有的内容容器:一个是子 ...