一. 结构体
1. 结构体简介
在 Swift 标准库中,绝大多数的公开类型都是结构体,而枚举和类只占很小一部分,如下:
所有的结构体都有一个编译器自动生成的初始化器(initializer .n 初始化方法、构造器、构造方法)
在第⑥行调用的,可以传入所有成员值,用以初始化所有成员(Stored Property .n 存储属性)
枚举:枚举可以使⽤rawValue来给枚举赋值,没有自动生成的初始化器
结构体:所有的结构体都有编译器⾃动⽣成的初始化器(也许不⽌⼀个),⽤于初始化所有成员,但是如果你⾃定义了初始化器,编译器就不会帮你了
类:编译器没有为类⾃动⽣成可以传⼊成员值的初始化器(想让我们⾃⼰写),但是如果属性都有默认值,也会帮我们创建⼀个⽆参初始化器
当然,枚举、结构体、类都可以自定义初始化器
2. 默认初始化器
编译器会根据情况,可能会为结构体生成多个初始化器,宗旨是:保证所有成员都有初始值
如果结构体的某个成员有默认值,初始化器初始化的时候可以不给这个成员设置值,反之,如果没默认值,那么一定要设置值。
- 思考:下面代码能编译通过吗?
可选项都有个默认值nil,因此可以编译通过
3. 自定义初始化器
一旦在定义结构体时自定义了初始化器,编译器就不会再帮它自动生成任何其他初始化器。
如上,自定义了初始化器,那么编译器就不会帮你自动生成任何其他初始化器。
4. 设置初始值的本质
结构体/类设置初始值,就相当于定义了一个无参初始化器。
以上两段代码完全等效。
下面通过窥探汇编,验证上面代码完全等效:
- 执行如下代码:
func test() {
struct Point {
var x: Int
var y: Int
init() {
x = 0
y = 0
}
}
var p = Point() //打断点
}
test()
断点暂停在下面这行,可以看出这行代码就是函数调用。
-> 0x10000245f <+15>: callq 0x100002480 ; init() -> Point #1 in TestSwift.test() -> () in Point #1 in TestSwift.test() -> () at main.swift:15
敲si,进入函数
(lldb) si
TestSwift`init() in Point #1 in test():
-> 0x100002480 <+0>: pushq %rbp
0x100002481 <+1>: movq %rsp, %rbp
0x100002484 <+4>: xorps %xmm0, %xmm0
0x100002487 <+7>: movaps %xmm0, -0x10(%rbp)
0x10000248b <+11>: movq $0x0, -0x10(%rbp) //将0赋值给x
0x100002493 <+19>: movq $0x0, -0x8(%rbp) //将0赋值给y
0x10000249b <+27>: xorl %eax, %eax
0x10000249d <+29>: movl %eax, %ecx
0x10000249f <+31>: movq %rcx, %rax
0x1000024a2 <+34>: movq %rcx, %rdx
0x1000024a5 <+37>: popq %rbp
0x1000024a6 <+38>: retq
调用了init()函数。
- 将上面代码改成:
func test() {
struct Point {
var x: Int = 0
var y: Int = 0
}
var p = Point() //打断点
}
test()
上面的操作再来一遍,结果如下:
-> 0x10000245f <+15>: callq 0x100002480 ; init() -> Point #1 in TestSwift.test() -> () in Point #1 in TestSwift.test() -> () at main.swift:15
TestSwift`init() in Point #1 in test():
-> 0x100002480 <+0>: pushq %rbp
0x100002481 <+1>: movq %rsp, %rbp
0x100002484 <+4>: xorps %xmm0, %xmm0
0x100002487 <+7>: movaps %xmm0, -0x10(%rbp)
0x10000248b <+11>: movq $0x0, -0x10(%rbp) //将0给x
0x100002493 <+19>: movq $0x0, -0x8(%rbp) //将0给y
0x10000249b <+27>: xorl %eax, %eax
0x10000249d <+29>: movl %eax, %ecx
0x10000249f <+31>: movq %rcx, %rax
0x1000024a2 <+34>: movq %rcx, %rdx
0x1000024a5 <+37>: popq %rbp
0x1000024a6 <+38>: retq
可以发现,两次生成的汇编代码一模一样,验证了:结构体/类设置初始值,就相当于定义了一个无参初始化器。
5. 结构体内存结构
执行如下代码:
func testStruct() {
struct Point {
var x = 10
var y = 20
var b = true
}
var p = Point(x: 11, y: 22, b: false)
print(Mems.memStr(ofVal: &p))
print(MemoryLayout<Point>.size)
print(MemoryLayout<Point>.stride)
print(MemoryLayout<Point>.alignment)
}
testStruct()
打印:
0x000000000000000b 0x0000000000000016 0x0000000000000000
17
24
8
可以看出前8字节存放10,后8字节存放20,最后1个字节存放0
实际需要8 + 8 + 1 = 17字节,由于内存对齐是8,所以系统分配24字节,和我们想的一样。
二. 类
1. 类简介
类的定义和结构体类似,但编译器并没有为类自动生成可以传入成员值的初始化器
2. 无参初始化器
如果类的所有成员都在定义的时候指定了初始值,编译器会为类生成无参的初始化器,成员的初始化是在这个初始化器中完成的。
以上两段代码完全等效,上面结构体已经验证过了,这里就不验证了,因为是类似的。
这也验证了我们上面所说的:结构体/类设置初始值,就相当于定义了一个无参初始化器。
3. 自定义初始化器
编译器没有为类⾃动⽣成可以传⼊成员值的初始化器(想让我们⾃⼰写),但是如果属性都有默认值,也会帮我们创建⼀个⽆参初始化器。
三. 结构体与类的区别
1. 本质区别
结构体是值类型(枚举也是值类型),类是引用类型(指针类型)
struct Point {
var x = 3
var y = 4
}
class Size {
var width = 1
var height = 2
}
//创建结构体和类
func test() {
var point = Point()
var size = Size()
}
运行如上代码,创建结构体和类之后,内存分布如下:
结构体和指针在栈空间,栈空间存放的指针指向的是对象的地址值,系统⾃⼰管理。(上面的point结构体占用16字节,前8字节放3,后8字节放4)。
对象和闭包在堆空间,我们⾃⼰管理,不过ARC模式下,编译器会⾃动帮我们加release。(上面的size对象占用32字节,每8字节分别存放指向类型信息、引用计数、1、2)。
2. 值类型
值类型赋值给var、let或者给函数传参,是直接将所有内容拷贝一份,类似于对文件进行copy、paste操作,产生了全新的文件副本,属于深拷贝(deep copy)。
如下代码:
内存中分布如下:
问:执行如下代码之后,p1.x和p1.y是多少?
p2.x = 11
p2.y = 22
答:因为是值传递,所以修改p2不会影响p1,所以p1:(10,20)、p2:(11,22)
- Copy On Write
- 在Swift标准库中,为了提升性能,String、Array、Dictionary、Set采取了Copy On Write的技术,比如仅当有“写”操作时,才会真正执行拷贝操作,否则都是浅拷贝(就是只拷贝指针)。
- 对于标准库值类型的赋值操作,Swift能确保最佳性能,所有没必要为了保证最佳性能来避免赋值
- 建议不需要修改的,尽量定义成let
- 值类型的赋值操作
3. 引用类型
引用赋值给var、let或者给函数传参,是将内存地址拷贝一份,类似于制作一个文件的替身(快捷方式、链接),指向的是同一个文件,属于浅拷贝(shallow copy)。
结果如下,由于是引用类型,所以修改s2,s1指向的内容也被修改了。
s1.width==s2.width==10
s2.height==s2.height==20
内存情况如下:
意思就是两个指针指向了同一个对象。
- 引用类型的赋值操作
意思就是s1指针,以前指向一个对象,重新赋值之后重新指向了另外一个对象。
4. 值类型、引用类型的let
可以看出:值类型的let,什么都不能改。引⽤类型的let,不能指向其他对象 (保存的指针地址不能变)。
四. 对象的堆空间申请过程
上面说了,结构体和指针在栈空间,对象和闭包在堆空间,这里验证一下:
内存区域、Tagged Pointer里面说过,通过alloc、malloc、calloc等动态分配的空间在堆里面,所以我们只需要证明创建对象后有调用alloc、malloc方法就可以。
运行代码:
func testClassAndStruct() {
class Size {
var width = 1
var height = 2
}
struct Point {
var x = 3
var y = 4
}
var point = Point() //断点
// var size = Size()
}
testClassAndStruct()
对于结构体来说,汇编函数调用比较简单,这里就省略了,并没有调用alloc、malloc等方法,说明结构体不在堆空间,在栈空间。
重新运行上面代码,敲si指令,一步一步查看汇编代码,根据如下流程:
在Swift中,创建类的实例对象,要向堆空间申请内存,大概流程如下:
Class.__allocating_init()
libswiftCore.dylib:_swift_allocObject_
libswiftCore.dylib:swift_slowAlloc
libsystem_malloc.dylib:malloc
最后调用了malloc,说明对象的确在堆空间。
注意:
- MemoryLayout用来查看枚举和结构体内存使⽤
- class_getInstanceSize用来查看实例对象,至少占⽤多少内存(内存对齐后的)
- malloc_size用来查看实例对象,系统实际分配的内存大小
下面的Mems.size内部其实也是调用malloc_size来获取实际占用内存大小
下面类的实例对象占用多少内存?
class Point {
var x = 11
var test = true
var y = 22
}
var p = Point()
print(class_getInstanceSize(type(of: p))) // 40
print( class_getInstanceSize(Point.self)) // 40
print(Mems.size(ofRef: p)) // 48
运行上面代码,打印40,说明p对象至少占用40字节内存(指向类型信息占用8,引用计数占用8,x占用8,y占用8,test占用1,实际一共占33字节,由于内存对齐是8,所以至少占用40),系统最终分配48字节(因为对象占用内存大小必须是16的倍数)
- 在Mac、iOS中的malloc函数分配的内存大小总是16的倍数
//传入一个指针,告诉你指针指向堆空间占用内存多大,如果传入不是堆空间的指针就返回0
var ptr = malloc(17) //传入你想要多少字节
print(malloc_size(ptr))
当传入1打印16,传入16打印16,传入17打印32。
- 类和结构体的比较
func testClassAndStruct() {
class Size {
var width = 1
var height = 2
}
struct Point {
var x = 3
var y = 4
}
print("------------------------")
// 类
var size = Size()
//至少占用32
print(class_getInstanceSize(Size.self)) //32
//系统最终分配32
print(Mems.size(ofRef: size)) //32
print("size变量的地址", Mems.ptr(ofVal: &size))
print("size变量的内存", Mems.memStr(ofVal: &size))
print("size所指向内存的地址", Mems.ptr(ofRef: size))
print("size所指向内存的内容", Mems.memStr(ofRef: size))
print("------------------------")
// 结构体
var point = Point()
//至少占用16
print("MemoryLayout<Point>.stride", MemoryLayout<Point>.size) //16
//系统最终分配16
print("MemoryLayout<Point>.stride", MemoryLayout<Point>.stride) //16
//内存对齐8
print("MemoryLayout<Point>.stride", MemoryLayout<Point>.alignment) //8
print("point变量的地址", Mems.ptr(ofVal: &point))
print("point变量的内存", Mems.memStr(ofVal: &point))
}
testClassAndStruct()
打印:
------------------------
32
32
size变量的地址 0x00007ffeefbff540
size变量的内存 0x00000001018008b0
size所指向内存的地址 0x00000001018008b0
size所指向内存的内容 0x00000001000089d8 0x0000000200000002 0x0000000000000001 0x0000000000000002
(分别对应:指向类型信息、引用计数、1、2)
------------------------
16
point变量的地址 0x00007ffeefbff510
point变量的内存 0x0000000000000003 0x0000000000000004
(分别对应:3、4)
通过打印可以看出:
- size对象占用32字节,可以使用上面的两种方式打印出来
- size所指向内存的内容 0x00000001000089d8 0x0000000200000002 0x0000000000000001 0x0000000000000002,分别对应:指向类型信息、引用计数、1、2
- point结构体系统实际分配16字节
- point变量的内存 0x0000000000000003 0x0000000000000004,存放的分别是3、4的值
注意:获取指针变量指向地址占用多少内存不能用如下方式:
print("MemoryLayout<Size>.stride", MemoryLayout<Size>.stride)
MemoryLayout是看某种类型变量占用多少内存,传入Size,由于Size是类,所以Size变量是指针类型,所以最后都当成指针,所以打印永远是8字节。
注意:结构体存放在哪取决于你在哪定义的。
- 如果结构体是在函数里面定义的那结构体就在栈空间
- 如果结构体是在函数外面定义的,那么它的内存就在数据段
- 如果类里面定义了结构体,那么这个结构体肯定跟随对象在堆空间
- 但是类,无论你在哪创建类,类的实例对象的内存一定在堆空间
- 但是类的实例对象对应的指针变量的内存在哪就不一定了,和上面类似。如果不明白可以参考:内存区域、Tagged Pointer
五. 其他补充
1. 结构体嵌套枚举
2. 枚举、结构体、类都可以定义方法
一般把定义在枚举、结构体、类内部的函数叫做方法,其他的都叫函数。
注意:方法不占用对象的内存,无论方法定义在哪里(即使在类内部),因为方法的本质就是函数,函数都存放在代码段。
3. 思考:以下结构体、类对象的内存结构是怎样的?
结构体:
func testStruct() {
struct Point {
var x: Int
var b1: Bool
var b2: Bool
var y: Int
}
var p = Point(x: 10, b1: true, b2: true, y: 20)
print("MemoryLayout<Size>.size", MemoryLayout<Point>.size) //18
print("MemoryLayout<Size>.stride", MemoryLayout<Point>.stride) //24
print("MemoryLayout<Size>.alignment", MemoryLayout<Point>.alignment) //8
}
testStruct()
可以发现,至少占用18字节,内存对齐8,系统实际分配24字节。两个Int占用8 + 8 = 16字节,两个Bool只需要2字节,一共18字节,对齐参数是8,所以占用24字节。
类:
func testClass() {
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
}
}
var s = Size(width: 10, b1: true, b2: true, height: 20)
print(class_getInstanceSize(type(of: s))) // 40
print( class_getInstanceSize(Size.self)) // 40
print(Mems.size(ofRef: s)) // 48
}
testClass()
可以发现,至少占用40字节,实际分配48字节。类型信息占用8字节,引用计数占用8字节,两个Int占用8 + 8 = 16字节,两个Bool只需要2字节,一共34字节,由于内存对齐是8,所以至少占用40字节,又由于对象占用内存大小必须是16倍数,所以实际占用48字节。