一、这里先看结构体和类的方法区别
先看看一个简单的结构体,可以打开断点调试,发现其调用方法的汇编非常简单 callq 0x100001410
struct Object {
func eat() { }
}
var o = Object()
o.eat() // 汇编:callq 0x100001410
再看看一个用类来实现相同功能,发现其调用方法的汇编就变的复杂起来 callq *0x70(%rcx)
,经过进一步跟进断点,发现内部做了非常多的中转操作
class Object {
func eat() { }
}
var o = Object()
o.eat() // 汇编:callq *0x70(%rcx)
实际上结构体和类的方法都存储在全局区,和全全局方法没有本质区别,只不过是编译器语法糖特性限制了我们的访问权限而已
可以看出,结构体方法调用非常简单,编译期就决定了调用地址,直接call+地址,而类的方法调用则比价复杂,因为要考虑到继承多态等动态特性,调用地址并不能在编译其确定
因此在设计时,如果某个类只是做一些简单的工作而不考虑复杂的事情时,优先选择结构体
二、对象的内存布局
对于一个对象,内存涉及到三块
- 1.对象地址内存(可能在堆空间、栈空间、全局区,指向堆空间)
- 2.对象本身内存(堆空间:分为3块)
2.1 类信息地址(指向全局区)
a.类的某些信息
b.类的对象方法列表(一般按定义顺序存储)
2.2 引用计数相关
2.3 属性列表
- 3.对象信息内存(全局区:分为2块)
3.1 对象信息
3.2 对象方法地址列表
如Object类的一个对象
class Object {
var age = 10
func eat() {
print("\(type(of: self))-->\(#function)")
}
func run() {
print("\(type(of: self))-->\(#function)")
}
func jump() {
print("\(type(of: self))-->\(#function)")
}
}
此处创建两个对象
var obj1 = Object()
var obj2 = Object()
Swift中,其对象方法实现类似C++虚表方式,其内存布局如下
虚表(类似C++)【全局区】
┏━━━━━━━━━━━━━━━━━━━━━━
┃ 类的某些信息
┗━━━━━━━━━━━━━━━━━━━━━━
┃ eat()方法地址
┗━━━━━━━━━━━━━━━━━━━━━━
对象地址 对象内存【堆空间】 ┃ run()方法地址
┏━━━━━┓ ┏━━━━━━━━━━━━━┓ ┗━━━━━━━━━━━━━━━━━━━━━━
┃ obj1┃━━>┃ classInfo ┃━━>┃ jump()方法地址
┗━━━━━┛ ┗━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━━━━━━━
┃ retainCount ┃ ↑
┗━━━━━━━━━━━━━┛ ┃
┃ var age ┃ ┃
┗━━━━━━━━━━━━━┛ ┃
┃ var name ┃ ┃
┗━━━━━━━━━━━━━┛ ┃
┃
┃
对象地址 对象内存【堆空间】 ┃
┏━━━━━┓ ┏━━━━━━━━━━━━━┓ ┃
┃ obj2┃-->┃ classInfo ┃━━━━━━┛
┗━━━━━┛ ┗━━━━━━━━━━━━━┛
┃ retainCount ┃
┗━━━━━━━━━━━━━┛
┃ var age ┃
┗━━━━━━━━━━━━━┛
┃ var name ┃
┗━━━━━━━━━━━━━┛
综上所属
- 1.所有同类型的对象共享一份虚表(方法列表)
- 2.对象本身的存储属性和引用计数包含在对象内存中
- 3.如果子类重写了父类的方法,则子类虚表中存储重写过的方法地址,如果子类没有重写,则子类虚表中会存储父类原本的方法地址