【iOS】Swift系列六 - 结构体与类的本质区别

在Swift标准库中,绝大多数的公开类型都是结构体,而枚举和类只占很小一部分。

一、结构体

常见的Bool、Int、Double、String、Array、Dictionary等常见类型都是结构体。

自定义结构体:

1
2
3
4
5
6
struct Date {
var year: Int;
var month: Int;
var day: Int;
}
var date = Date(year: 2019, month: 06, day: 02)

所有结构体都有一个编译器自动生成的初始化器(initializer、初始化方法、构造器、构造方法)。

Date(year: 2019, month: 06, day: 02)传入的是所有成员值,用来初始化所有成员(叫做存储属性)。

1.1. 结构体的初始化器

编译器会根据情况,可能会为结构体生成多个初始化器,宗旨是:保证所有成员都有初始值。

从上面案例可以看出,编译器帮助生成初始化器的条件就是:让所有存储属性都有值。

思考:下面的代码能否编译通过?

可选项都有个默认值nil,所以可以编译通过。

1.2. 自定义初始化器

一旦在定义结构体时自定义了初始化器,编译器就不会再帮它自动生成其他初始化器。

1.3. 探究结构体初始化器的本质

下面的两段代码是等效的:

代码一:

代码二:

经过对比发现,代码一和代码二的init方法完全一样。也就是说,存储属性的初始化是在初始化构造方法中完成的。

1.4. 结构体的内存结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Point {
var x = 10
var y = 20
var b = true
}
var p = Point()
print(Mems.memStr(ofVal: &p))
print(MemoryLayout<Point>.size)
print(MemoryLayout<Point>.stride)
print(MemoryLayout<Point>.alignment)

/*
输出:
0x000000000000000a 0x0000000000000014 0x0000000000000001
17
24
8
*/

因为存储属性xy各占8个字节(连续内存地址),Bool在内存中占用1个字节,所以Point一共占用17个字节,由于内存对齐是8,所以一共分配了24个字节。

二、类

类的定义和结构体类似,但编译器并没有为类自动生成可以传入成员值的初始化器。

定义类:

如果存储属性没有初始值,无参的初始化器也不会自动生成:

如果把上面的类换成结构体(struct)类型就不会报错:

2.1. 类的初始化器

如果类的所有成员都在定义的时候指定了初始值,编译器会为类生成无参的初始化器。

成员的初始化是在这个初始化器中完成的。

下面的两段代码是等效的:
代码一:

1
2
3
4
5
6
class Point {
var x: Int = 0
var y: Int = 0
}

var p1 = Point()

代码二:

1
2
3
4
5
6
7
8
9
10
class Point {
var x: Int
var y: Int
init() {
self.x = 0
self.y = 0
}
}

var p1 = Point()

三、结构体与类的本质区别

结构体时值类型(枚举也是值类型),类是引用类型(指针类型)。

3.1. 内存分析结构体与类

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Size {
var width: Int = 1
var height: Int = 2
}

struct Point {
var x: Int = 3
var y: Int = 4
}

func test() {
var size = Size()
print("class-size对象的内存",Mems.memStr(ofRef: size))
print("class-size指针的内存地址",Mems.ptr(ofVal: &size))
print("class-size对象的内存地址",Mems.ptr(ofRef: size))
print("class-size.width的内存地址",Mems.ptr(ofVal: &size.width))
print("class-size.height的内存地址",Mems.ptr(ofVal: &size.height))
var point = Point()
print("struct-point对象的内存",Mems.memStr(ofVal: &point))
print("struct-point的内存地址",Mems.ptr(ofVal: &point))
print("struct-point.x的内存地址",Mems.ptr(ofVal: &point.x))
print("struct-point.y的内存地址",Mems.ptr(ofVal: &point.y))
}
test()
/*
输出:
class-size对象的内存 0x00000001000092a8 0x0000000200000002 0x0000000000000001 0x0000000000000002
class-size指针的内存地址 0x00007ffeefbff4d0
class-size对象的内存地址 0x000000010061fe80
class-size.width的内存地址 0x000000010061fe90
class-size.height的内存地址 0x000000010061fe98

struct-point对象的内存 0x0000000000000003 0x0000000000000004
struct-point的内存地址 0x00007ffeefbff470
struct-point.x的内存地址 0x00007ffeefbff470
struct-point.y的内存地址 0x00007ffeefbff478
*/

示例代码的在内存中:

经过分析可以看到,结构体的数据是直接存到栈空间的,类的实例是用指针指向堆空间的内存,指针在栈空间。上面示例代码中类的实例占用32个字节,其中前面16个字节分别存储指向类型信息和引用计数,后面16个字节才是真正用来存储数据的。而结构体占用的内存大小等于存储属性所占内存大小之和。

注意:在C语言中,结构体是不能定义方法的,但是在C++Swift中,可以在结构体和类中定义方法。在64bit环境中,指针占用8个字节。

扩展:值类型(结构体、枚举)的内存根据所处的位置不同,内存的位置也不一样。例如,定义一个全局的结构体,内存在数据段(全局区)中;如果在函数中定义,内存存放在栈空间;如果在类中定义一个结构体,内存跟随对象在堆空间。

3.2. 汇编分析结构体与类

Swift中,创建类的实例对象,要向堆空间申请内存,大概流程如下:

  • Class.__allocating_init()
  • libswiftCore.dylib:_swift_allocObject_
  • libswiftCore.dylib:swift_slowAlloc
  • libsystem_malloc.dylib:malloc

在Mac,iOS中的malloc函数分配的内存大小总是16的倍数(为了做内存优化)。

通过class_getInstanceSize可以得知类的对象真正使用的内存大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Foundation

class Point {
var x: Int = 3
var y: Int = 4
var b: Bool = true
}
var p = Point()
print(class_getInstanceSize(type(of: p)))
print(class_getInstanceSize(Point.self))
/*
输出:
40
40
*/

内存占用大小 = 8(指向类型信息) + 8(引用计数) + 8(存储属性x) + 8(存储属性y) + 1(存储属性b) = 33;

内存分配大小 = 8(指向类型信息) + 8(引用计数) + 8(存储属性x) + 8(存储属性y) + Max(1(存储属性b), 8(内存对齐数)) = 40;

扩展:如果底层调用了alloc或malloc函数,说明该对象存在堆空间,否则就是在栈空间。

3.2.1. 汇编分析结构体

第一步:创建结构体,打断点进入汇编:

第二步:在callq...init()函数处进入函数实现体(lldb进入函数体指令:si):

结论:rbp就是局部变量,所以结构体创建的对象是在栈中存储的。

扩展:一般情况下,rbp就是局部变量,rip是全局变量,ret是函数返回。

3.2.2. 汇编分析类

第一步:创建结构体,打断点进入汇编:

第二步:在callq...__allocating_init()...函数处打断点,进入函数体:

第三步:在callq...swift_allocObject函数处打断点,进入函数体:

第四步:一直进入到libswiftCore.dylib swift_allocObject:中,在callq...swift_slowAlloc处打断点进入:

第五步:malloc出现了,这时候继续进入函数体:

第六步:最终,对象是在libsystem_malloc.dylib库中执行的malloc

经过上面分析,可以清晰的看到,对象是在堆空间存储的。

扩展:在Mac、iOS中,创建对象都是调用的libsystem_malloc.dylib动态库。