结构体

在Swift标准库中,绝大多数的公开类型都是结构体,而枚举和类只占很小的一部分
比如Bool、Int、Double、String、Array、Dictionary等常见类型都是结构体

  1. struct Date {
  2. var year: Int
  3. var month: Int
  4. var day: Int
  5. }
  6. let date = Date(year: 2000, month: 10, day: 10)

所有的结构体都有一个编译器自动生成的初始化器(initializer,初始化器、构造器、构造方法)
在第6行调用的,可以传入所有成员值,用以初始化所有成员,year、month、day是存储属性(Stored Property)

结构体的初始化器

编译器会根据情况,可能会为结构体生成多个初始化器,宗旨是:保证所有成员都有初始值
image.png
p2、p3、p4会报错,因为没有给所有成员设置初始值

  1. struct Point {
  2. var x: Int = 0
  3. var y: Int = 0
  4. }
  5. var p1 = Point(x: 0, y: 0)
  6. var p2 = Point(x: 10)
  7. var p3 = Point(y: 10)
  8. var p4 = Point()

上面代码则不会报错,因为已经设置了初始值。

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

  1. struct Point {
  2. var x: Int?
  3. var y: Int?
  4. }
  5. var p1 = Point(x: 0, y: 0)
  6. var p2 = Point(x: 10)
  7. var p3 = Point(y: 10)
  8. var p4 = Point()

是可以的,因为可选项默认值是nil

自定义初始化器

一旦在定义结构体时自定义了初始化器,编译器就不会再帮它自动生成其他初始化器
image.png
因为自定义了初始化器,所有p2、p3、p4这些初始化器是会成功的。

窥探初始化器的本质

观察未添加自定义初始化器,和添加了初始化器的两个结构体初始化时的汇编代码

  1. func testStruct() {
  2. struct Point {
  3. var x: Int = 0
  4. var y: Int = 0
  5. }
  6. var p = Point()
  7. }
  8. func testStruct() {
  9. struct Point {
  10. var x: Int
  11. var y: Int
  12. init() {
  13. x = 0
  14. y = 0
  15. }
  16. }
  17. var p = Point()
  18. }
  19. testStruct()
  1. SwiftTest`init() in Point #1 in testStruct():
  2. -> 0x100003c40 <+0>: pushq %rbp
  3. 0x100003c41 <+1>: movq %rsp, %rbp
  4. 0x100003c44 <+4>: xorps %xmm0, %xmm0
  5. 0x100003c47 <+7>: movaps %xmm0, -0x10(%rbp)
  6. 0x100003c4b <+11>: movq $0x0, -0x10(%rbp)
  7. 0x100003c53 <+19>: movq $0x0, -0x8(%rbp)
  8. 0x100003c5b <+27>: xorl %eax, %eax
  9. 0x100003c5d <+29>: movl %eax, %edx
  10. 0x100003c5f <+31>: movq %rdx, %rax
  11. 0x100003c62 <+34>: popq %rbp
  12. 0x100003c63 <+35>: retq

都会调用init方法,所以自己添加初始化器和系统自动添加的初始化器底层逻辑是相同的。

结构体的内存结构

  1. struct Point {
  2. var x: Int = 0
  3. var y: Int = 0
  4. var origin: Bool = false
  5. }
  6. print(MemoryLayout<Point>.size) // 17
  7. print(MemoryLayout<Point>.stride) // 24
  8. print(MemoryLayout<Point>.alignment) // 8

结构体由两个Int类型(8个字节),1个Bool类型(1个字节)组成,实际占用17个字节,由于内存对齐,系统分配了24个,对齐单位是8

类的定义和结构体类似, 但编译器并没有为类自动生成可以传入成员值的初始化器
image.png
如果成员没有默认值,那么无参初始化器不能调用
image.png

类的初始化器

如果类的所有成员都在定义的时候制定了初始值,编译器会为类生成无参的初始化器
成员的初始化是在这个初始化器中完成的

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

上面2段代码是完全等效的

结构体和类的本质区别

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

  1. struct Point {
  2. var x = 3
  3. var y = 4
  4. }
  5. class Size {
  6. var width = 1
  7. var height = 2
  8. }
  9. func test() {
  10. var p = Point()
  11. var s = Size()
  12. }

创建一个结构体对象p和类对象s,在内存中的结构如下:
image.png

证明

查看Point()时的汇编代码,没有调用alloc/malloc方法

  1. SwiftTest`testStructAndClass():
  2. 0x100003ac0 <+0>: pushq %rbp
  3. 0x100003ac1 <+1>: movq %rsp, %rbp
  4. 0x100003ac4 <+4>: subq $0x10, %rsp
  5. 0x100003ac8 <+8>: xorps %xmm0, %xmm0
  6. 0x100003acb <+11>: movaps %xmm0, -0x10(%rbp)
  7. 0x100003acf <+15>: callq 0x100003650 ; SwiftTest.Point.init() -> SwiftTest.Point at main.swift:105
  8. -> 0x100003ad4 <+20>: movq %rax, -0x10(%rbp)
  9. 0x100003ad8 <+24>: movq %rdx, -0x8(%rbp)
  10. 0x100003adc <+28>: addq $0x10, %rsp
  11. 0x100003ae0 <+32>: popq %rbp
  12. 0x100003ae1 <+33>: retq

查看Size()时的汇编代码,通过观察可知,调用了malloc方法

  1. SwiftTest`testStructAndClass():
  2. 0x100003aa0 <+0>: pushq %rbp
  3. 0x100003aa1 <+1>: movq %rsp, %rbp
  4. 0x100003aa4 <+4>: pushq %r13
  5. 0x100003aa6 <+6>: pushq %rax
  6. 0x100003aa7 <+7>: movq $0x0, -0x10(%rbp)
  7. 0x100003aaf <+15>: xorl %eax, %eax
  8. 0x100003ab1 <+17>: movl %eax, %edi
  9. -> 0x100003ab3 <+19>: callq 0x100003ae0 ; type metadata accessor for SwiftTest.Size at <compiler-generated>
  10. 0x100003ab8 <+24>: movq %rax, %r13
  11. 0x100003abb <+27>: callq 0x100003a40 ; SwiftTest.Size.__allocating_init() -> SwiftTest.Size at main.swift:110
  12. 0x100003ac0 <+32>: movq %rax, -0x10(%rbp)
  13. 0x100003ac4 <+36>: movq -0x10(%rbp), %rdi
  14. 0x100003ac8 <+40>: callq 0x100003d40 ; symbol stub for: swift_release
  15. 0x100003acd <+45>: addq $0x8, %rsp
  16. 0x100003ad1 <+49>: popq %r13
  17. 0x100003ad3 <+51>: popq %rbp
  18. 0x100003ad4 <+52>: retq

所以size对象向堆空间申请了内存

值类型

值类型赋值给var、let或者给函数传参,是直接将所有内容拷贝一份
类似于对文件进行copy、paste操作,产生了全新的副本。属于深拷贝(deep copy)

  1. struct Point {
  2. var x: Int
  3. var y: Int
  4. }
  5. func test() {
  6. let p1 = Point(x: 10, y: 20)
  7. let p2 = p1
  8. }

内存布局如下:
image.png

值类型的赋值操作

  1. let s1 = "a"
  2. var s2 = s1
  3. s2.append("_c")
  4. print(s1, s2) // a a_c

在Swift标准库中,为了提升性能,String、Array、Dictionary,Set采用了Copy On Write
如上面的例子,如果s2没有被修改的话,s2和s1指向同一快内存地址,相当于浅拷贝,只有当修改s2时,才会进行深拷贝。
对于标准库值类型的赋值操作,Swift能确保最佳性能,所以没必要为了保证最佳性能来避免赋值
建议:不需要修改的,尽量定义为let
下面代码不会开辟新的内存,会覆盖p1原有的内存进行赋值

  1. struct Point {
  2. var x: Int
  3. var y: Int
  4. }
  5. var p1 = Point(x: 10, y: 20)
  6. p1 = Point(x: 11, y: 22)

引用类型

引用类型赋值给var、let或者给函数传参,时将内存地址拷贝一份
类似于制作一个替身(快捷方式、链接),指向的是同一个文件。数据浅拷贝(shadow copy)

  1. class Size {
  2. var width: Int
  3. var height: Int
  4. init(width: Int, height: Int) {
  5. self.width = width
  6. self.height = height
  7. }
  8. }
  9. func test() {
  10. let s1 = Size(width: 10, height: 20)
  11. var s2 = s1
  12. }

内存布局如下:
image.png

引用类型的赋值操作

  1. var s1 = Size(width: 10, height: 20)
  2. s1 = Size(width: 11, height: 21)

对s1重新赋值,栈空间的地址指向新的堆空间内存,旧的堆空间内存被释放。(引用计数)

值类型、引用类型let

image.png
image.png
通过观察,值传递的常量都是保存在栈空间,无论地址还是内容都不可以修改。
引用类型的常量修饰保的是存在栈空间的指针,不可以修改,但是可以修改堆空间的内容。
image.png
字符串和数组定义为let后不可以修改内容

对象的堆空间申请过程

在Swift中,创建类的实例对象,要向堆空间申请内存,大概流程如下
Class._allocating_init()
libswiftCore.dylib:_swift_allocObject

libswiftCore.dylib:swift_slowAlloc
libsystem_malloc.dylib:malloc
在Mac、iOS中的malloc函数分配的内存大小总是16的倍速
通过class_getInstanceSize可以得知,累的对象至少需要占用多少内存

  1. class Point {
  2. // 指向类型信息、引用计数占用16个字节
  3. var x: Int = 10 // 占用8个字节
  4. var y: Int = 20 // 占用8个字节
  5. var origin: Bool = true // 占用1个字节
  6. }
  7. var p = Point()
  8. print(Mems.size(ofRef: p)) // 48

总共33个字节,根据内存对齐应该是40,但是根据iOS内存对齐最小单位是16,所以系统分配了48个字节

嵌套类型

  1. struct Level {
  2. enum Good {
  3. case A, B, C
  4. }
  5. enum Bad {
  6. case D, E, F
  7. }
  8. }
  9. var good = Level.Good.A
  10. var bad = Level.Bad.D

枚举、结构体、类都可以定义方法

  1. class Size {
  2. var width = 10
  3. var height = 20
  4. func log() {
  5. print("width = \(width), height = \(height)")
  6. }
  7. }
  8. struct Point {
  9. var x = 10
  10. var y = 20
  11. func log() {
  12. print("x = \(x), height = \(y)")
  13. }
  14. }
  15. enum Level {
  16. case good
  17. case bad
  18. func log() {
  19. print("good = \(Level.good), bad = \(Level.bad)")
  20. }
  21. }

方法占用内存吗?
不占用
方法的本质就是函数
方法和函数都存放在代码段