swift查看内存地址小工具Mems
https://github.com/CoderMJLee/Mems.git
1.枚举
1.1 观察枚举所占内存情况
enum Season{
case spring,
summer,
autumn,
winter,
unknown
}
var s = Season.autumn
var val = s.rawValue
// s占用了多少字节
print("s占用了" + "\(MemoryLayout.size(ofValue: s))" + "字节")
// 系统实际给s分配了多少个字节
print("系统实际给s分配了" + "\(MemoryLayout.stride(ofValue: s))" + "字节")
// 内存对齐的字节数长度
print("内存对齐的字节长度" + "\(MemoryLayout.alignment(ofValue: s))" + "字节")
系统实际给s分配了1字节
s实际占用了1字节
内存对齐的字节长度1字节
Program ended with exit code: 0
在没有原始值和关联值的情况下,枚举只占用一个字节。我们可以通过汇编查看这一个字节保存了什么
Snip20190722_1.png
AT&T汇编中MOV为赋值指令,MOV后面的字母为操作数长度,b(byte)为一个字节。$代表着字面量,%开头的是CPU的寄存器。 movb $0x2, 0x500f(%rip)这一句汇编代码的意思就是将2这个常量赋值给寄存器%rip中的地址加上0x500f(也就是枚举变量S的地址)。寄存器中%rip保存的是下一条汇编代码的地址,所以0x500f(%rip)=0x100001579 + 0x500f = 0x100006588。
通过xcode查看0x100006588中保存的值
Snip20190722_2.png
通过比对其他4个枚举在内存中的值,可以发现枚举在内存中的值和定义顺序有关。
spring为0,summer为1,autumn为2,winter为3,unkonwn为4,且占用内存为1个字节。
1.2 原始值是如何影响枚举内存的
enum Season:String{
case spring = "spring",
summer = "summer",
autumn = "autumn",
winter = "winter",
unknown = "unknown"
}
var s = Season.autumn
var val = s.rawValue
// s占用了多少字节
print("s占用了" + "\(MemoryLayout.size(ofValue: s))" + "字节")
// 系统实际给s分配了多少个字节
print("系统实际给s分配了" + "\(MemoryLayout.stride(ofValue: s))" + "字节")
// 内存对齐的字节数长度
print("内存对齐的字节长度" + "\(MemoryLayout.alignment(ofValue: s))" + "字节")
系统实际给s分配了1字节
s实际占用了1字节
内存对齐的字节长度1字节
Program ended with exit code: 0
观察打印结果,我们发现原始值并不会影响枚举内存的大小,通过汇编可以窥探到swift是通过了rawValue这个计算属性的getter方法返回的原始值
Snip20190723_1.png
上图的callq是调用0x100002700所在的函数。注释写的很清楚,这个函数名为Season.rawValue.getter
Snip20190723_2.png
getter方法的内部使用了一个switch语句,根据case来判断应该跳转返回什么原始值。
jmpq *%rdx 代表着跳转到%rdx中代码段的地址,而rdx是根据枚举值等一系列计算得出的,意味着会根据枚举值执行相应的switch代码段返回对应的原始值。
1.3 关联值是如何影响枚举内存的
enum Season{
case spring(Int,Int,Int),
summer(String,String,String),
autumn(Bool,Bool,Bool),
winter(Int,Int),
unknown(Bool)
}
//01 00 00 00 00 00 00 00
//01 00 00 00 00 00 00 00
//01 00 00 00 00 00 00 00
//00 00 00 00 00 00 00 00
//00 00 00 00 00 00 00 00
//00 00 00 00 00 00 00 00
//00 00 00 00 00 00 00 00
var spring = Season.spring(1, 1, 1)
// s占用了多少字节
print("spring占用了" + "\(MemoryLayout.size(ofValue: spring))" + "字节")
// 系统实际给spring分配了多少个字节
print("系统实际给spring分配了" + "\(MemoryLayout.stride(ofValue: spring))" + "字节")
// 内存对齐的字节数长度
print("内存对齐的字节长度" + "\(MemoryLayout.alignment(ofValue: spring))" + "字节")
spring占用了49字节
系统实际给spring分配了56字节
内存对齐的字节长度8字节
//61 00 00 00 00 00 00 00
//00 00 00 00 00 00 00 E1
//61 00 00 00 00 00 00 00
//00 00 00 00 00 00 00 E1
//61 00 00 00 00 00 00 00
//00 00 00 00 00 00 00 E1
//01 00 00 00 00 00 00 00
var summer = Season.summer("a", "a", "a")
// summer占用了多少字节
print("summer占用了" + "\(MemoryLayout.size(ofValue: summer))" + "字节")
// 系统实际给summer分配了多少个字节
print("系统实际给summer分配了" + "\(MemoryLayout.stride(ofValue: summer))" + "字节")
// 内存对齐的字节数长度
print("内存对齐的字节长度" + "\(MemoryLayout.alignment(ofValue: summer))" + "字节")
summer占用了49字节
系统实际给summer分配了56字节
内存对齐的字节长度8字节
//01 01 01 00 00 00 00 00
//00 00 00 00 00 00 00 00
//00 00 00 00 00 00 00 00
//00 00 00 00 00 00 00 00
//00 00 00 00 00 00 00 00
//00 00 00 00 00 00 00 00
//02 00 00 00 00 00 00 00
var autumn = Season.autumn(true, true, true)
// summer占用了多少字节
print("autumn占用了" + "\(MemoryLayout.size(ofValue: autumn))" + "字节")
// 系统实际给summer分配了多少个字节
print("系统实际给autumn分配了" + "\(MemoryLayout.stride(ofValue: autumn))" + "字节")
// 内存对齐的字节数长度
print("内存对齐的字节长度" + "\(MemoryLayout.alignment(ofValue: autumn))" + "字节")
autumn占用了49字节
系统实际给autumn分配了56字节
内存对齐的字节长度8字节
观察打印结果,可以发现枚举的内存大小受关联值的影响,也就是说枚举的关联值是存储在枚举内部的。
比如 Spring(Int,Int,Int)一个Int在x86架构下占8个字节,那么其3个Int类型的关联值就是24个字节,还要加上一个字节存放枚举值,所以是25个字节,又因为内存对齐的长度是8个字节,系统分配给Spring这个枚举的内存应当是8的倍数,所以会分配32个字节给Spring。但是打印结果告诉我们Spring占用了49个字节,系统实际分配了56个字节,这是为什么呢?
因为枚举值分配的空间是按照最大的枚举值来分配的,例子中Season类型的枚举summer(String,String,String)需要占用49个字节(一个Stirng占16个字节,3 * 16 + 1 = 49),所以Season会给所有的枚举值分配49个字节,并在第49个字节存放枚举值。由于内存对齐长度为8个字节,系统分配的内存必须为8的倍数。所以系统会分配56个字节给Season类型的枚举值。
单个枚举所占空间是按照枚举关联值所占字节总和最高的枚举字节数+1个字节的方式来分配的。
枚举结语
在没有关联值的情况下,枚举在内存中占1个字节且所占内存的大小不受原始值影响。原始值以计算属性的方式存在枚举中,调用rawValue属性会通过switch的方式返回相应的原始值。关联值会保存在枚举的内存中,影响着枚举所占内存的大小。说到这里我们也可以明白为什么枚举不能定义存储属性了,因为枚举中一旦保存计算属性会和枚举的关联值产生歧义。
2.类
class Animal{
var age:Int = 0
var height:Int = 10
// var weight:Int = 20
init() {
}
}
var animal = Animal.init()
// 查看animal对象所占内存
print(MemoryLayout.stride(ofValue: animal))
// 查看animal对象实际所占内存
print(MemoryLayout.size(ofValue: animal))
// 查看animal对象内存对齐的字节数长度
print(MemoryLayout.alignment(ofValue: animal))
打印结果
8
8
8
Program ended with exit code: 0
无论往Person对象中增加还是减少存储属性,通过MemoryLayout类方法打印出的内存占用都是8个字节,这是因为Animal对象存储在堆中,animal变量存储在全局区,animal变量内部保存着Animal对象的内存地址,MemoryLayout打印的是animal这个变量所占用的内存,所以无论如何打印出来的都是swift指针大小,也就是8个字节。
那我们如何查看Animal对象的大小呢?
第一种方法是通过汇编查看,如图。
Snip20190723_1.png
或者我们可以直接通过开头介绍的小工具,打印Animal对象的地址
print(Mems.ptr(ofRef: animal))
打印结果
0x100708730
Animal对象实际占用24个字节,由于堆空间内存对齐的长度为16个字节,意味着Animal对象占用的内存必须为16的倍数,所以系统实际给Animal对象分配了32个字节
98 65 00 00 01 00 00 00 类型信息
02 00 00 00 00 00 00 00 引用计数信息(此处的引用计数是系统通过对该值计算得出的,而不是保存的2)
0A 00 00 00 00 00 00 00 age变量
10 90 45 57 FF 7F 00 00 堆空间的内存对齐长度为16个字节,此8个字节为脏数据
通过内存数据观察,我们可以很直观的看到第17~24个字节保存着age变量。
但是如何证明前8个字节是类型信息,第9~16个字节保存的是引用计数呢?
我们先来证明第9~16个字节保存的是引用计数
我们用四个变量指向Animal对象,并打上断点,观察每一个增加一个变量指向Aniaml对象后,Animal对象的内存是如何变化的。
var animal = Animal.init()
var animal1 = animal
var animal2 = animal
var animal3 = animal
print(Mems.ptr(ofRef: animal))
// 1个变量指向Aniaml对象,Animal对象第9~16个字节存储的数据
02 00 00 00 00 00 00 00
// 2个变量指向Aniaml对象,Animal对象第9~16个字节存储的数据
02 00 00 00 02 00 00 00
// 3个变量指向Aniaml对象,Animal对象第9~16个字节存储的数据
02 00 00 00 04 00 00 00
// 4个变量指向Aniaml对象,Animal对象第9~16个字节存储的数据
02 00 00 00 06 00 00 00
通过内存观察,我们发现,Animal对象的第9~16个字节随着指向其变量的增加密切变动,可以证明其存储的数据和引用计数相关。
我们接着来证明1~8个字节保存的是类的类型信息,为了看得更清楚一些,我写了一个子类继承Animal,代码如下:
class Animal{
var age:Int = 10
init() {}
func breath() {
print("animal breath")
}
func eat() {
print("animal eat")
}
}
class Person:Animal{
var name:String?
override func breath() {
print("person breath")
}
override func eat() {
print("person eat")
}
}
var animal:Animal = Animal.init()
print(Mems.ptr(ofRef: animal))
animal.breath()
animal.eat()
animal = Person()
print(Mems.ptr(ofRef: animal))
animal.breath()
animal.eat()
我们通过汇编观察Animal对象是如何调用方法的
Snip20190723_3.png
关键汇编代码如下:
通过汇编注释我们很明显的发现 0x57df(%rip)这个地址保存的值是Animal对象的前8个字节,movq指令意味着将%rip寄存器中保存的地址加上0x57df后得到一个新的地址并将第1~8个字节的值赋值给了寄存器%rax。
0x100001022 <+418>: movq 0x57df(%rip), %rax ; command.animal : command.Animal
然后又将%rax的值赋值给了%rcx
0x100001029 <+425>: movq %rax, %rcx
最后将%rcx的值加上0x78,得出一个函数地址值,并且调用这个函数。通过前面的分析我们知道%rcx中保存的其实就是animal对象的前8个字节,animal.breath()的函数地址值是根据animal对象前8个字节的值的偏移0x78得出的,animal.eat()的函数地址值是根据animal对象前8个字节的值的偏移0x80得出的。
0x100001058 <+472>: callq *0x78(%rcx) // animal.breath()
0x1000010bf <+575>: callq *0x80(%rcx) // animal.eat()
我们再来看下当animal指针指向Animal的子类对象Person后,子类对象Person是如何调用父类Animal的对象方法breath和eat的
Snip20190724_6.png
0x1000012f2 <+1138>: callq *0x78(%rcx) // person.breath()
0x10000135f <+1247>: callq *0x80(%rcx) // person.eat()
此时%rcx中保存的其实就是Person对象的前8个字节,Person.breath()的函数地址值是根据Person对象前8个字节的值的偏移0x78得出的,Person.eat()的函数地址值是根据Person对象前8个字节的值的偏移0x80得出的。
由此得出,class的对象的前8个字节保存着type的meta data,其中包括了方法的地址。
类结语
类的结构相比结构体要复杂一些,由于类的实例对象保存在堆空间中,系统需要通过检查引用计数的情况来确定是否需要回收对象(ARC中系统已经帮我们处理堆内存的管理,程序员不需要关心引用计数,但这并不代表引用计数不存在),所以对象中需要留出8个字节保存引用计数情况。类可以被继承,由于面向对象语言的多态特性,在调用类的实例对象方法时,编译器需要动态地获取对象方法所在的函数地址,所以需要留出8个字节保存类的类型信息,比如对象方法的地址就保存在类型信息中。
所以当类的实例对象在调用对象方法时,性能的开销相比结构体以及枚举调用方法要大,因为多态的存在,系统会先找到该对象的前8个字节(type meta data)加上一个偏移值得到函数的地址,再找到这个函数去调用。
3.结构体
3.1 观察结构体所占内存情况
struct Person {
var age:Int = 10
var man:Bool = true
func test() {
print("test")
}
}
let per = Person()
// 查看结构体per所占内存
print(MemoryLayout.stride(ofValue: per))
// 查看结构体per实际所占内存
print(MemoryLayout.size(ofValue: per))
// 查看结构体per内存对齐的字节数长度
print(MemoryLayout.alignment(ofValue: per))
打印结果
16
9
8
Program ended with exit code: 0
由于结构体是值类型,相较于类而言其不能被子类继承,也不需要引用计数来管理其内存的释放。所以在存储属性相同的情况下,结构体的内存要比类小。再来看一下结构体的方法调用
struct Person {
var age:Int = 10
var man:Bool = true
func test() {
print("test")
}
}
let per = Person()
per.test()
Snip20190724_1.png
结构体由于不能继承,其方法地址在编译的时候就能确定。
0x100001a54 <+52>: callq 0x100001b20
网友评论