Swift结构体和类的本质

一、前言

在swift中,结构体和类是比较常用的两种结构,他们之间有很多类似点,比如声明属性、方法、扩展等,相关语法可参考:swiftGswift翻译G-类和结构体。结构Swift体和类也有不同点,其本质的区别就是,结构体是值类型,类是引用类型。

一般我们都知道,在C语言中,像变量是什么意思一些基本类型结构如int、char等都是值类型,他们的特点就是在被传递时,会被拷贝,产生一份新的值。而指针类型就是对应的引用类型,一个指针指向了一段内存地址,在传递的时候,新的指针指向的内存地址没有变化,对于新指针指向内存内容的变量类型有哪些修改,通过旧初始化sdk什么意思指针也可以访问到。

今天主要通过汇编代码来验证下,在swift中,结构体和类的区别。

二、内存布局

2.1 结构体

2.1.1 简单例子

首先来看看结构体的内存布局,来看一段代码

struct Point {
    var x: Int
    var y: Int
}
func test() {
    var p1 = Point(x: 2, y: 3)
}

首先猜测,x和y分别各占用8个字节,结构体应该需要占用16个字节。

通过MemoryLayoutswift是什么意思啊法,可以打印出Point结构体占用的实际内存为16个字节,通过汇编代码来确认下(以下仅保留关键部分代码):

TestSwift`test():
movl   $0x2, %edi ; 函数传参2
movl   $0x3, %esi ; 函数传参3
callq  0x1000034a0 ; 调用初始化方法,传参分别为edi和esi,TestSwift.Point.init(x: Swift.Int, y: Swift.Int)
movq   %rax, -0x10(%rbp) ; 然后结果rax压入栈rbp中
movq   %rdx, -0x8(%rbp) ; 然后结果rax压入栈rbp中
retq   

通过x/16xb打印出rbp-0x10的内容如成员变量和静态变量的区别变量类型有哪些

0x7ffeefbff430: 0x02 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7ffeefbff438: 0x03 0x00 0x00 0x00 0x00 0x00 0x00 0x00

可以看到,在栈中有16字节保存了0x02和0x03这两个内容,也就是结构体p1的内存布局情况变量与函数(因为是小端模式初始化英文,所以0x02和0x03在最前边)。

2.1.2 复杂例子

再来看一个更复杂的例子,代码如下,总共定义了4个结构体

struct Point {
    var x: Int
    var b1: Bool
    var b2: Bool
    var y: Int
}
struct Point2 {
    var x: Bool
    var y: Bool
}
struct Point3 {
    var x: Int
    var b1: Bool
    var y: Int
    var b2: Bool
}
struct Point4 {
    var x: Int
    var y: Int
    var b1: Bool
    var b2: Bool
}
var p1 = Point(x: 2, b1: true, b2: true, y: 3)
var p2 = Point2(x: true, y: true)
var p3 = Point3(x: 4, b1: true, y: 5, b2: true)
var p4 = Point4(x: 6, y: 7, b1: true, b2: true)

每个结构体的内存占用情况,这里需要空间是实际变量占用的空间,分配空间是实际运行时分配的空变量英语间,取决于内存对齐字节。

--------Point1----------
需要空间:24 字节
分配空间: 24 字节
对齐字节:8 字节
--------Point2----------
需要空间:2 字节
分配空间: 2 字节
对齐字节:1 字节
--------Point3----------
需要空间:25 字节
分配空间: 32 字节
对齐字节:8 字节
--------Point4----------
需要空间:18 字节
分配空间: 24 字节
对齐字节:8 字节

每个Point的内存情况(依然是小端模式)

Point1 分配24个字节
x:     02 00 00 00 00 00 00 00 
b1,b2: 01 01 00 00 00 00 00 00 
y:     03 00 00 00 00 00 00 00
Point2 分配2个字节
b1,b2: 01 01
Point3 分配32个字节
x:  04 00 00 00 00 00 00 00 
b1: 01 00 00 00 00 00 00 00 
y:  05 00 00 00 00 00 00 00 
b2: 01 00 00 00 00 00 00 00
Point4 分配24个字节
x:     06 00 00 00 00 00 00 00 
y:     07 00 00 00 00 00 00 00 
b1,b2: 01 01 00 00 00 00 00 00

总结如下:

  • 结构体中字段的排列顺序不同,会有不同的大小占用

  • 每一个字段的起点,必须是这个字段大小的整数倍,比如Point3中,b1虽然只占用1个字节,但y也是从第17个字节开始,等于说b1实际占用了8个字节,有7个字节浪费了

所以在定义结构指针数学成员变量和局部变量区别的时候,相同大小的变量可以尽量放到一起,这样可以节省占用的空间。

2.2 类

结构体的内存布局是比较简单的,而且因swift翻译为是值类型,所以会直接存放在栈中,也就是值类型的特点。

我们再看一看类的内存布局情况,代码如下,定义了4个成员变量,初始化方法,以及一个calculate方法。

class Size {
    var width: Int
    var b1: Bool
    var b2: Bool
    var height: Int
    init(width: Int, b1: Bool, b2: Bool, height: Int) {
        self.width = width
        self.b1 = b1
        self.b2 = b2
        self.height = height
    }
    func calculate() -> Int{
        return width + height
    }
}
var s = Size(width: 4, b1: true, b2: true, height: 5)
var result = s.calculate()

汇编代码如下

callq  0x100003230 ; 调用初始化方法TestSwift.Size.__allocating_init(width: Swift.Int, b1: Swift.Bool, b2: Swift.Bool, height: Swift.Int)
movq   %rax, %r13 ; rax是方法返回值,也就是创建的s对象,放到了r13中
movq   %r13, -0x28(%rbp) ; 将返回的指针放入栈中,也就是代码中的s
movq   (%r13), %rax ; r13存储的是s的对象地址,这里是间接寻址,将r13存储地址指向的内存放入rax,也就是s的前8个字节,这里是0x000000010000c2e8
movq   0xd8(%rax), %rax ; 将rax加0xD8的地址对应的内存值给rax,最终结果是0x100003410,对应的就是TestSwift.Size.calculate()函数
callq  *%rax ; 调用rax指向的内存,也就是调用calculate方法
movq   -0x28(%rbp), %rdi
movq   %rax, -0x20(%rbp) ; 返回值在rax中,此时存储的值为9,也就是4+5的结果

在汇编代码中,调用初始化方法后,rax寄存器里存储的是类对象的地址,地址指swift语言向的是堆内存地址,代表实例对象实际占用的空间成员变量是什么意思。这个就是引指针万用表读数图解用类型的特点。

打印一下堆内存的信息如下,s对象实际需要40,变量之间的关系但分配了48个字节,字节对齐是16字节

E8 C2 00 00 01 00 00 00 获取类型信息,查找类实例方法的首地址
03 00 00 00 00 00 00 00 引用计数
04 00 00 00 00 00 00 00 width:4
01 01 9B 80 FF 7F 0F 00 前两位是两个bool,后边的值应该无意义,也不会被用到
05 00 00 00 00 00 00 00 height:5

2.3 修改变量

通过汇编来了解下,在修改结构体或类对象的变量时,具体是如何做的

2.3.1 结构体的修改

首先代码如下

struct Point {
    var x: Int
    var y: Int
}
test()
func test() {
    var p1 = Point(x: 10, y: 20)
    var p2 = p1
    p2.x = 11
    p2.y = 22
}

汇编代码解析

TestSwift`test():
movl   $0xa, %edi ; 将10放到edi
movl   $0x14, %esi ; 将20放到esi
callq  0x100003480 ; 调用init方法 TestSwift.Point.init(x: Swift.Int, y: Swift.Int)
movq   %rax, -0x10(%rbp) ; rax是返回值,内容是0x0a,也就是10,放入栈中即p1.x
movq   %rdx, -0x8(%rbp) ; rdx也是返回值,内容是0x14,也就是20,放入栈中即p1.y
; 此刻p1的内存地址如下,栈地址为0x7FFEEFBFF430,总共占16个字节
; 0A 00 00 00 00 00 00 00 
; 14 00 00 00 00 00 00 00
movq   %rax, -0x20(%rbp) ; 因为有p1和p2,所以栈中还需要再放一次rax和rdx,证明是值引用
movq   %rdx, -0x18(%rbp) 
; 此刻p2的内存地址如下,栈地址为0x7ffeefbff420总共占16个字节
; 0A 00 00 00 00 00 00 00 
; 14 00 00 00 00 00 00 00
; p2的栈地址比p1小16,因为栈的结构特点,所以高地址的应该是更早压入栈的,所以p1在高地址,p2在低地址,也即p2比p1地址小
movq   $0xb, -0x20(%rbp) ; 将11放入rbp-0x20位置,所以rbp-0x20是p2
movq   $0x16, -0x18(%rbp) ; 同理这个是赋值22
retq   

总结如下:

结构体的字段是直接放在函数栈中的,对于p2.x和p2.y的修改,就是直接将Swift对应的值11和12放到栈中该结构体字段对应的位置即可。

2.3.2 类的修改

代码如下

class Size {
    var width: Int
    var height: Int
    init(width: Int, height: Int) {
        self.width = width
        self.height = height
    }
}
test()
func test() {
    var s1 = Size(width: 10, height: 20)
    var s2 = s1
    s2.width = 11
    s2.height = 22
}

汇编代码解析

TestSwift`test():
movl   $0xa, %edi ; 将10放入edi,作为函数参数
movl   $0x14, %esi ; 将20放入esi,作为函数参数
callq  0x1000033e0; 调用allocating init申请堆空间 TestSwift.Size.__allocating_init(width: Swift.Int, height: Swift.Int)   
movq   %rax, %r13 ; rax是返回值,值为0x10615d250,代表前边申请的堆地址
; 此时类的内存情况如下,总共32个字节
; A8 C2 00 00 01 00 00 00 获取类型信息,查找类实例方法的首地址
; 03 00 00 00 06 00 00 00 应该是引用计数
; 0A 00 00 00 00 00 00 00 存储的是10
; 14 00 00 00 00 00 00 00 存储的是11
movq   %r13, -0x20(%rbp) ; 对应s2,rbp是局部变量,rbp-0x20存储的是s1的地址,就是0x10615d250
movq   %r13, -0x10(%rbp) ; 对应s1,里边存的地址和s2是一样的,也证明了类是引用类型
movq   %r13, -0x18(%rbp)
movq   (%r13), %rax ; 这里是一个间接寻址,将r13指向内存的第一个8字节赋值给rax,打印结果:rax = 0x10000c2a8  type metadata for TestSwift.Size,也就是类内存的前8个字节,可参考前边内存分布
movq   0x68(%rax), %rax ; rax+0x68,打印结果rax = 0x100003180  TestSwift`TestSwift.Size.width.setter : Swift.Int at <compiler-generated>,看起来就是找到setter方法
movl   $0xb, %edi ; 准备开始将11赋值给width,这一步是将11赋值到edi,作为setter的入参
callq  *%rax ; 调用setter width方法
movq   -0x20(%rbp), %r13 ; 将s1的内存首地址重新给r13
movq   (%r13), %rax
movq   0x80(%rax), %rax ; 不用怀疑,这个就是setter height的方法地址
movl   $0x16, %edi ; 这个是需要赋值给height的22,放到edi中作为函数参数
callq  *%rax ; 调用height方法
retq   

总结如下:

  • 类实例对象的堆空间成员变量和局部变量中,前8个字节存储了类的一些相关信息,地址为0x10000c2a8,通过对该地成员变量址的偏移可以找到对应的实例方法,参考movq 0x68(%rax), %rax 这一句
  • 找到实例方法setter后就很简单,指针式万用表调用方法修改参数即可

三、总结

值类型的内容会直接存储在内存中,比如函数的栈中,而引用类型需要成员变量和局部变量区别一个指针中转,在函数栈中指针存储引用对象的指针地址,该指针存储的地址指向的堆内存,才是该对象真实的存储,一般是在堆内存中。通过汇初始化sdk什么意思编也验证和学习了我们修改变量时是如何做的。

四、参考

本文内容指针万用表怎么读数参考了小码哥的Swiswift翻译ft编程从入门到精通,有兴趣的也可以学习下。

评论

发表回复