结构体
在Swift标准库中,绝大多数的公开类型都是结构体,而枚举和类只占很小的一部分
比如Bool、Int、Double、String、Array、Dictionary等常见类型都是结构体
struct Date {
var year: Int
var month: Int
var day: Int
}
let date = Date(year: 2000, month: 10, day: 10)
所有的结构体都有一个编译器自动生成的初始化器(initializer,初始化器、构造器、构造方法)
在第6行调用的,可以传入所有成员值,用以初始化所有成员,year、month、day是存储属性(Stored Property)
结构体的初始化器
编译器会根据情况,可能会为结构体生成多个初始化器,宗旨是:保证所有成员都有初始值
p2、p3、p4会报错,因为没有给所有成员设置初始值
struct Point {
var x: Int = 0
var y: Int = 0
}
var p1 = Point(x: 0, y: 0)
var p2 = Point(x: 10)
var p3 = Point(y: 10)
var p4 = Point()
思考:下面的代码能编译通过吗?
struct Point {
var x: Int?
var y: Int?
}
var p1 = Point(x: 0, y: 0)
var p2 = Point(x: 10)
var p3 = Point(y: 10)
var p4 = Point()
自定义初始化器
一旦在定义结构体时自定义了初始化器,编译器就不会再帮它自动生成其他初始化器
因为自定义了初始化器,所有p2、p3、p4这些初始化器是会成功的。
窥探初始化器的本质
观察未添加自定义初始化器,和添加了初始化器的两个结构体初始化时的汇编代码
func testStruct() {
struct Point {
var x: Int = 0
var y: Int = 0
}
var p = Point()
}
func testStruct() {
struct Point {
var x: Int
var y: Int
init() {
x = 0
y = 0
}
}
var p = Point()
}
testStruct()
SwiftTest`init() in Point #1 in testStruct():
-> 0x100003c40 <+0>: pushq %rbp
0x100003c41 <+1>: movq %rsp, %rbp
0x100003c44 <+4>: xorps %xmm0, %xmm0
0x100003c47 <+7>: movaps %xmm0, -0x10(%rbp)
0x100003c4b <+11>: movq $0x0, -0x10(%rbp)
0x100003c53 <+19>: movq $0x0, -0x8(%rbp)
0x100003c5b <+27>: xorl %eax, %eax
0x100003c5d <+29>: movl %eax, %edx
0x100003c5f <+31>: movq %rdx, %rax
0x100003c62 <+34>: popq %rbp
0x100003c63 <+35>: retq
都会调用init方法,所以自己添加初始化器和系统自动添加的初始化器底层逻辑是相同的。
结构体的内存结构
struct Point {
var x: Int = 0
var y: Int = 0
var origin: Bool = false
}
print(MemoryLayout<Point>.size) // 17
print(MemoryLayout<Point>.stride) // 24
print(MemoryLayout<Point>.alignment) // 8
结构体由两个Int类型(8个字节),1个Bool类型(1个字节)组成,实际占用17个字节,由于内存对齐,系统分配了24个,对齐单位是8
类
类的定义和结构体类似, 但编译器并没有为类自动生成可以传入成员值的初始化器
如果成员没有默认值,那么无参初始化器不能调用
类的初始化器
如果类的所有成员都在定义的时候制定了初始值,编译器会为类生成无参的初始化器
成员的初始化是在这个初始化器中完成的
class Point {
var x: Int = 10
var y: Int = 20
}
class Point {
var x: Int
var y: Int
init() {
x = 10
y = 20
}
}
结构体和类的本质区别
结构体是值类型(枚举也是值类型),类是引用类型(指针类型)
struct Point {
var x = 3
var y = 4
}
class Size {
var width = 1
var height = 2
}
func test() {
var p = Point()
var s = Size()
}
证明
查看Point()时的汇编代码,没有调用alloc/malloc方法
SwiftTest`testStructAndClass():
0x100003ac0 <+0>: pushq %rbp
0x100003ac1 <+1>: movq %rsp, %rbp
0x100003ac4 <+4>: subq $0x10, %rsp
0x100003ac8 <+8>: xorps %xmm0, %xmm0
0x100003acb <+11>: movaps %xmm0, -0x10(%rbp)
0x100003acf <+15>: callq 0x100003650 ; SwiftTest.Point.init() -> SwiftTest.Point at main.swift:105
-> 0x100003ad4 <+20>: movq %rax, -0x10(%rbp)
0x100003ad8 <+24>: movq %rdx, -0x8(%rbp)
0x100003adc <+28>: addq $0x10, %rsp
0x100003ae0 <+32>: popq %rbp
0x100003ae1 <+33>: retq
查看Size()时的汇编代码,通过观察可知,调用了malloc方法
SwiftTest`testStructAndClass():
0x100003aa0 <+0>: pushq %rbp
0x100003aa1 <+1>: movq %rsp, %rbp
0x100003aa4 <+4>: pushq %r13
0x100003aa6 <+6>: pushq %rax
0x100003aa7 <+7>: movq $0x0, -0x10(%rbp)
0x100003aaf <+15>: xorl %eax, %eax
0x100003ab1 <+17>: movl %eax, %edi
-> 0x100003ab3 <+19>: callq 0x100003ae0 ; type metadata accessor for SwiftTest.Size at <compiler-generated>
0x100003ab8 <+24>: movq %rax, %r13
0x100003abb <+27>: callq 0x100003a40 ; SwiftTest.Size.__allocating_init() -> SwiftTest.Size at main.swift:110
0x100003ac0 <+32>: movq %rax, -0x10(%rbp)
0x100003ac4 <+36>: movq -0x10(%rbp), %rdi
0x100003ac8 <+40>: callq 0x100003d40 ; symbol stub for: swift_release
0x100003acd <+45>: addq $0x8, %rsp
0x100003ad1 <+49>: popq %r13
0x100003ad3 <+51>: popq %rbp
0x100003ad4 <+52>: retq
值类型
值类型赋值给var、let或者给函数传参,是直接将所有内容拷贝一份
类似于对文件进行copy、paste操作,产生了全新的副本。属于深拷贝(deep copy)
struct Point {
var x: Int
var y: Int
}
func test() {
let p1 = Point(x: 10, y: 20)
let p2 = p1
}
值类型的赋值操作
let s1 = "a"
var s2 = s1
s2.append("_c")
print(s1, s2) // a a_c
在Swift标准库中,为了提升性能,String、Array、Dictionary,Set采用了Copy On Write
如上面的例子,如果s2没有被修改的话,s2和s1指向同一快内存地址,相当于浅拷贝,只有当修改s2时,才会进行深拷贝。
对于标准库值类型的赋值操作,Swift能确保最佳性能,所以没必要为了保证最佳性能来避免赋值
建议:不需要修改的,尽量定义为let
下面代码不会开辟新的内存,会覆盖p1原有的内存进行赋值
struct Point {
var x: Int
var y: Int
}
var p1 = Point(x: 10, y: 20)
p1 = Point(x: 11, y: 22)
引用类型
引用类型赋值给var、let或者给函数传参,时将内存地址拷贝一份
类似于制作一个替身(快捷方式、链接),指向的是同一个文件。数据浅拷贝(shadow copy)
class Size {
var width: Int
var height: Int
init(width: Int, height: Int) {
self.width = width
self.height = height
}
}
func test() {
let s1 = Size(width: 10, height: 20)
var s2 = s1
}
引用类型的赋值操作
var s1 = Size(width: 10, height: 20)
s1 = Size(width: 11, height: 21)
对s1重新赋值,栈空间的地址指向新的堆空间内存,旧的堆空间内存被释放。(引用计数)
值类型、引用类型let
通过观察,值传递的常量都是保存在栈空间,无论地址还是内容都不可以修改。
引用类型的常量修饰保的是存在栈空间的指针,不可以修改,但是可以修改堆空间的内容。
字符串和数组定义为let后不可以修改内容
对象的堆空间申请过程
在Swift中,创建类的实例对象,要向堆空间申请内存,大概流程如下
Class._allocating_init()
libswiftCore.dylib:_swift_allocObject
libswiftCore.dylib:swift_slowAlloc
libsystem_malloc.dylib:malloc
在Mac、iOS中的malloc函数分配的内存大小总是16的倍速
通过class_getInstanceSize可以得知,累的对象至少需要占用多少内存
class Point {
// 指向类型信息、引用计数占用16个字节
var x: Int = 10 // 占用8个字节
var y: Int = 20 // 占用8个字节
var origin: Bool = true // 占用1个字节
}
var p = Point()
print(Mems.size(ofRef: p)) // 48
总共33个字节,根据内存对齐应该是40,但是根据iOS内存对齐最小单位是16,所以系统分配了48个字节
嵌套类型
struct Level {
enum Good {
case A, B, C
}
enum Bad {
case D, E, F
}
}
var good = Level.Good.A
var bad = Level.Bad.D
枚举、结构体、类都可以定义方法
class Size {
var width = 10
var height = 20
func log() {
print("width = \(width), height = \(height)")
}
}
struct Point {
var x = 10
var y = 20
func log() {
print("x = \(x), height = \(y)")
}
}
enum Level {
case good
case bad
func log() {
print("good = \(Level.good), bad = \(Level.bad)")
}
}
方法占用内存吗?
不占用
方法的本质就是函数
方法和函数都存放在代码段