一、概述
本系列文章旨在复习Swift5
核心语法且适当进行底层原理探索,属于阶段性复习和巩固,以供日后进一步探索Swift
语言的底层原理做铺垫。
整个系列文章如下,每一文章知识点独立成篇,欢迎各位按需或按兴趣点击阅读:
- 01-📝Swift5常用核心语法|了解Swift【Swift简介、Swift的版本、Swift编译原理】
- 02-📝Swift5常用核心语法|基础语法【Playground、常量与变量、常见数据类型、字面量、元组、流程控制、函数、枚举、可选项、guard语句、区间】
- 03-📝Swift5常用核心语法|面向对象【闭包、结构体、类、枚举】
- 04-📝Swift5常用核心语法|面向对象【属性、inout、类型属性、单例模式、方法、下标、继承、初始化】
- 05-📝Swift5常用核心语法|高级语法【可选链、协议、错误处理、泛型、String与Array、高级运算符、扩展、访问控制、内存管理、字面量、模式匹配】
- 06-📝Swift5常用核心语法|编程范式与Swift源码【从OC到Swift、函数式编程、面向协议编程、响应式编程、Swift源码分析】
二、 闭包
1. 闭包表达式(Closure Expression)
-
- 在Swift中,可以通过
func
定义一个函数,也可以通过闭包表达式
定义一个函数
- 在Swift中,可以通过
-
- 闭包表达式格式如下:
{ (参数列表) -> 返回值类型 in 函数体代码 }
var fn = { (v1: Int, v2: Int) -> Int in return v1 + v2 } fn(10, 20) { (v1: Int, v2: Int) -> Int in return v1 + v2 }(10, 20)
-
- 闭包表达式的简写如下:
- case1
var fn = { (v1: Int, v2: Int) -> Int in return v1 + v2 } var fn: (Int, Int) -> Int = { $0 + $1 }
- case2
func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) { print(fn(v1, v2)) } exec(v1: 10, v2: 20) { (v1, v2) -> Int in return v1 + v2 } exec(v1: 10, v2: 20, fn: { (v1, v2) -> Int in return v1 + v2 }) exec(v1: 10, v2: 20, fn: { (v1, v2) -> Int in v1 + v2 }) exec(v1: 10, v2: 20, fn: { v1, v2 in return v1 + v2 }) exec(v1: 10, v2: 20, fn: { v1, v2 in v1 + v2 }) exec(v1: 10, v2: 20, fn: { $0 + $1 }) exec(v1: 10, v2: 20, fn: +)
2. 尾随闭包
-
- 如果将一个很长的闭包表达式作为
函数的最后一个实参
,使用尾随闭包可以增强函数的可读性
- 如果将一个很长的闭包表达式作为
-
- 尾随闭包是一个被书写在函数调用括号外面(后面)的闭包表达式
func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) { print(fn(v1, v2)) } exec(v1: 10, v2: 20) { $0 + $1 }
-
- 如果闭包表达式是函数的唯一实参,而且使用了尾随闭包的语法,那就不需要在函数名后边写圆括号
func exec(fn: (Int, Int) -> Int) { print(fn(1, 2)) } exec(fn: { $0 + $1 }) exec() { $0 + $1 } exec { $0 + $1 } exec { _, _ in 10 }
Swift中的
sort函数
用来排序的,使用的就是闭包的写法![]()
var nums = [11, 2, 18, 6, 5, 68, 45]
//1.
nums.sort()
//2.
nums.sort { (i1, i2) -> Bool in
i1 < i2
}
//3.
nums.sort(by: { (i1, i2) in return i1 < i2 })
//4.
nums.sort(by: { (i1, i2) in return i1 < i2 })
//5.
nums.sort(by: { (i1, i2) in i1 < i2 })
//6.
nums.sort(by: { $0 < $1 })
//7.
nums.sort(by: <)
//8.
nums.sort() { $0 < $1 }
//9.
nums.sort { $0 < $1 }
//10.
print(nums) // [2, 5, 6, 11, 18, 45, 68]
3. 闭包的定义(Closure)
严谨的定义是:
- 一个函数和它所捕获的
变量/常量
环境组合起来,称为闭包- 一般指定义在函数内部的函数
- 一般它捕获的是外层函数的局部变量\常量
- 全局变量,全局都可以访问,内存只有一份,且只要程序不停止运行,其内存就不会回收
typealias Fn = (Int) -> Int func getFn() -> Fn { var num = 0 func plus(_ i: Int) -> Int { num += i return num } return plus }
func getFn() -> Fn { var num = 0 return { num += $0 return num } }
var fn1 = getFn() var fn2 = getFn() fn1(1) // 1 fn2(2) // 2 fn1(3) // 4 fn2(4) // 6 fn1(5) // 9 fn2(6) // 12
通过汇编分析闭包的实现 看下面示例代码,分别打印为多少
func getFn() -> Fn {
var num = 0
func plus(_ i: Int) -> Int {
num += i
return num
}
return plus
}
var fn = getFn()
print(fn(1)) // 1
print(fn(2)) // 3
print(fn(3)) // 6
print(fn(4)) // 10
我们通过反汇编来观察:
通过这句调用可以看出:
- 在
return plus
之前,闭包(表层通过allicObject
)底层会调用malloc函数
进行堆内存的分配,也就是将拷贝num的值到堆上来持有不被释放 - 而栈里的num由于
getFn
调用完毕就随着栈释放了,plus函数
里操作的都是堆上的num - 调用
malloc函数
之前需要告诉系统要分配多少内存,需要24个字节来存储内存- (因为在iOS系统中,分配堆内存的底层算法有内存对齐的概念,内存对齐的参数是16)而通过
malloc函数
分配的内存都是大于或等于其本身数据结构所需内存的16的最小倍数,所以会分配32个字节内存
- (因为在iOS系统中,分配堆内存的底层算法有内存对齐的概念,内存对齐的参数是16)而通过
我们打印rax寄存器
的值可以知道:
- 系统分配的32个字节,前16个字节用来存储其他信息
- 而且从图上的圈起来的地方也可以看到,将0移动16个字节
- 所以16个字节之后的8个字节才用来存储num的值
调用fn(1)
,将断点打在这里,然后查看反汇编指令
然后调用到plus函数
内部,再次打印rax寄存器
的值,发现num的值已经变为1了
然后继续往下执行调用fn(2)
,发现num的值已经变为3了
然后继续往下执行调用fn(3)
,发现num的值已经变为6了
然后继续往下执行调用fn(4)
,发现num的值已经变为10了
闭包和类的相似之处
我们可以把闭包想像成是一个类的实例对象
- 内存在堆空间
- 捕获的局部变量\常量就是对象的成员(存储属性)
- 组成闭包的函数就是类内部定义的方法
类似如下示例
class Closure {
var num = 0
func plus(_ i: Int) -> Int {
num += i
return num
}
}
var cs = Closure()
cs.plus(1)
cs.plus(2)
cs.plus(3)
cs.plus(4)
而且通过反汇编也能看出类和闭包的共同之处:
- 分配的堆内存空间前16个字节都是用来存储
数据类型信息
和引用计数
的
再看下面的示例
如果把num变成全局变量呢,还会不会分配堆内存
typealias Fn = (Int) -> Int
var num = 0
func getFn() -> Fn {
func plus(_ i: Int) -> Int {
num += i
return num
}
return plus
}
var fn = getFn()
print(fn(1)) // 1
print(fn(2)) // 3
print(fn(3)) // 6
print(fn(4)) // 10
我们通过反汇编可以看到,系统不再分配堆内存空间了
注意: 如果返回值是函数类型,那么参数的修饰要保持统一
func add(_ num: Int) -> (inout Int) -> Void {
func plus(v: inout Int) {
v += num
}
return plus
}
var num = 5
add(20)(&num)
print(num)
4. 自动闭包
我们先看下面的示例代码
如果调用getFirstPositive
并传入两个参数,第一个参数符合条件,但是还需要调用plus
来得到第二个参数,这种设计相比就稍许有些浪费了
// 如果第1个数大于0,返回第一个数。否则返回第2个数
func getFirstPositive(_ v1: Int, _ v2: Int) -> Int {
v1 > 0 ? v1 : v2
}
func plus(_ num1: Int, _ num2: Int) -> Int {
print("haha")
return num1 + num2
}
getFirstPositive(10, plus(2, 4))
我们进行了一些优化,将第二个参数的类型变为函数,只有条件成立的时候才会去调用
func getFirstPositive(_ v1: Int, _ v2: () -> Int) -> Int {
v1 > 0 ? v1 : v2()
}
func plus(_ num1: Int, _ num2: Int) -> Int {
print("haha")
return num1 + num2
}
getFirstPositive(10, { plus(2, 4)} )
这样确定能够满足条件避免多余的调用,但是可读性就会差一些
我们可以使用
自动闭包@autoclosure
来修饰形参
-
@autoclosure
会将传进来的类型包装成闭包表达式,这是编译器特性
-
@autoclosure
只支持() -> T
格式的参数
func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int { v1 > 0 ? v1 : v2() } func plus(_ num1: Int, _ num2: Int) -> Int { print("haha") return num1 + num2 } getFirstPositive(10, plus(2, 4))
-
@autoclosure
并非只支持最后一个参数
func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int, _ v3: Int) -> Int { v1 > 0 ? v1 : v2() }
-
空合并运算符??
中就使用了@autoclosure
来将??
后面的参数进行了包装
-
- 有
@autoclosure
和无@autoclosure
会构成函数重载,不会报错
func getFirstPositive(_ v1: Int, _ v2: () -> Int) -> Int { v1 > 0 ? v1 : v2() } func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int { v1 > 0 ? v1 : v2() }
- 有
注意:为了避免与期望冲突,使用了@autoclosure
的地方最好明确注释清楚:这个值会被推迟执行
5. 通过汇编进行底层分析
1.分析下面这个函数的内存布局
func sum(_ v1: Int, _ v2: Int) -> Int { v1 + v2 }
var fn = sum
print(MemoryLayout.stride(ofValue: fn)) // 16
反汇编之后
可以看到:
- 底层会先计算sum的值,然后移动到fn的前8个字节
- 再将0移动到fn的后8个字节,总共占用16个字节
两个地址相差8个字节,所以是连续的,都表示fn的前后8个字节的地址值
2.分析下面这个函数的内存布局
typealias Fn = (Int) -> Int
func getFn() -> Fn {
var num = 0
func plus(_ i: Int) -> Int {
return i
}
return plus
}
var fn = getFn()
print(Mems.size(ofVal: &fn)) // 16
反汇编之后
我们能看到:
-
- 先调用
getFn
- 先调用
-
- 之后
rax
和rdx
会给fn分配16个字节
- 之后
然后我们进入getFn
看看rax
和rdx
存储的值分别是什么
可以看到会将plus的返回值
放到rax
中
可以看到ecx
和自己进行异或运算,并把结果0存储到rdx
中
所以回过头看第一张图就知道了,fn的16
个字节中,前8个字节存储的是plus
的返回值,后8个字节存储的是0
等同于将plus函数
赋值给fn
var fn = plus()
3.分析下面这个函数的内存布局
我们将上面示例里的plus函数
内部对num进行捕获,看看其内存布局有什么变化
typealias Fn = (Int) -> Int
func getFn() -> Fn {
var num = 0
func plus(_ i: Int) -> Int {
num += i
return num
}
return plus
}
var fn = getFn()
fn(1)
fn(2)
fn(3)
print(Mems.size(ofVal: &fn)) // 16
反汇编之后
我们可以看到,调用完getFn
之后,会分别将rax
和rdx
的值移动到rip+内存地址
,也就是给全局变量fn进行赋值操作
我们通过打印获取fn的内存占用知道是16个字节,fn的前8个字节就是rax
里存储的值,而后8个字节存储的是rdx
里的值
我们只需要找到rax
和rdx
里分别存储的是什么就可以了
可以看到在堆空间分配完内存之后的rax
给上面几个都进行了赋值,最后的rdx
里存储的就是堆空间的地址值
从这句看rax
里存储的应该是和plus函数
相关,下面我们就要找到rax
里存储的是什么
而且我们调用fn(1)时也可以推导出是调用的全局变量fn的前八个字节
参数1会存储到edi
中
而经过上面的推导我们知道-0xf8(%rbp)
中存储的是fn的前8个字节,那么往后8位就是-0x100(%rbp)
,里面放的肯定就是堆空间的地址值了,存储到了r13
中
我们在这里打断点,来观察rax
里到底存储的是什么
经过一系列的跳转,重要来到了plus真正的函数地址
而且r13
最后给了rsi
,rdi
中存储的还是参数1
进到plus函数
中,然后找到进行相加计算的地方,因为传进来的参数是变化的,所以不可能是和固定地址值进行相加
通过推导得知rcx
里存储的值就是rdi
中的参数1
通过推导得知rdx
里存储的值就是rsi
中的堆内存的num地址
所以可以得知0x10(%rdx)
也就是rdx
跳过16个字节的值就是num的值
通过打印也可以证明我们的分析是正确的
通过推导可以发现rax
中存储的是rsi
的num的地址值
然后将rcx
中的值覆盖掉rax
中的num地址值
而且真正进行捕获变量的时机是在getFn
即将return之前做的事
4.分析下面这个函数的内存布局
我们来看下面这个闭包里的变量会被捕获几次
typealias Fn = (Int) -> (Int, Int)
func getFns() -> (Fn, Fn) {
var num1 = 0
var num2 = 0
func plus(_ i: Int) -> (Int, Int) {
num1 += i // 6 + 0 = 6, 1 + 4 = 5,
num2 += i << 1 // 1100 = 12 + 0 = 12, 1000 = 8 + 2 = 10
return (num1, num2)
}
func minus(_ i: Int) -> (Int, Int) {
num1 -= i // 6 - 5 = 1, 5 - 3 = 2
num2 -= i << 1 // 1010 = 12 - 10 = 2, 0110 = 10 - 6 = 4
return (num1, num2)
}
return (plus, minus)
}
let (p, m) = getFns()
print(p(6)) // 6, 12
print(m(5)) // 1, 2
print(p(4)) // 5, 10
print(m(3)) // 2, 4
反汇编之后
发现其底层分别会分配两个堆空间,但是num1、num2也只是分别捕获一次,然后两个函数plus、minus
共有
三、结构体
1. 基本概念
- 在Swift标准库中,绝大多数的公开类型都是结构体,而枚举和类只占很小一部分
- 比如
Bool、Int、String、Double、Array、Dictionary
等常见类型都是结构体struct Date { var year: Int var month: Int var day: Int } var date = Date(year: 2019, month: 6, day: 23)
- 比如
- 所有的结构体都有一个编译器自动生成的初始化器(
initializer
,初始化方法、构造器、构造方法)- 通过默认生成的初始化器初始化:传入所有成员值,用以初始化所有成员(
存储属性
,Stored Property
)var date = Date(year: 2019, month: 6, day: 23)
- 通过默认生成的初始化器初始化:传入所有成员值,用以初始化所有成员(
2. 结构体的初始化器
- 编译器会根据情况,可能会为结构体生成多个初始化器,宗旨是:
保证所有成员都有初始值
- 如果结构体的成员定义的时候都有默认值了,那么生成的初始化器不会报错
- 如果是下面这几种情况就会报错
- 如果是可选类型的初始化器也不会报错,因为可选类型默认的值就是
nil
- 如果是下面这几种情况就会报错
3. 自定义初始化器
我们也可以自定义初始化器
struct Point {
var x: Int = 0
var y: Int = 0
init(x: Int, y: Int) {
self.x = x
self.y = y
}
}
var p1 = Point(x: 10, y: 10)
下面对变量p2
、p3
、p4
初始化报错的原因是 因为我们 已经自定义初始化器了,编译器就不会再帮我们生成默认的初始化器了
4. 初始化器的本质
下面这两种写法是完全等效的
struct Point {
var x: Int = 0
var y: Int = 0
}
等效于
struct Point {
var x: Int
var y: Int
init() {
x = 0
y = 0
}
}
var p4 = Point()
- 我们通过反汇编分别对比一下两种写法的实现,发现也是一样的:
- 因此,我们不难得出结论:
默认初始化器的本质,就是给存储属性做了默认赋值工作(比如这里给Int类型的两个属性默认赋值为0
)
5.结构体的内存结构
- 我们通过打印,了解下结构体占用的内存大小 和 其 内存布局
struct Point {
var x: Int = 10
var y: Int = 20
}
var p4 = Point()
print(MemoryLayout<Point>.stride) // 16
print(MemoryLayout<Point>.size) // 16
print(MemoryLayout<Point>.alignment) // 8
print(Mems.memStr(ofVal: &p4)) // 0x000000000000000a 0x0000000000000014
通过控制台,我们可以看到:
- 系统一共分配了
16
个字节的内存空间 - 前8个字节存储的是10,后8个字节存储的是20
- 我们再看下面这个结构体
struct Point {
var x: Int = 10
var y: Int = 20
var origin: Bool = false
}
var p4 = Point()
print(MemoryLayout<Point>.stride) // 24
print(MemoryLayout<Point>.size) // 17
print(MemoryLayout<Point>.alignment) // 8
print(Mems.memStr(ofVal: &p4)) // 0x000000000000000a 0x0000000000000014 0x0000000000000000
可以看到:
- 结构体实际只用了17个字节,而因为系统分配有内存对齐的概念,所以分配了24个字节
- 前8个字节存储的是10,中间8个字节存储的是20,最后1个字节存储的是false,也就是0
四、类
- 类的定义和结构体类似,但编译器并没有为类自动生成可以传入成员值的初始化器
- 如果成员没有初始值,所有的初始化器都会报错
1. 类的初始化器
- 如果类的所有成员都在定义的时候指定了初始值,编译器会为类生成
无参的初始化器
- 成员的初始化是在这个初始化器中完成的
class Point { var x: Int = 0 var y: Int = 0 } let p1 = Point()
- 下面这两种写法是完全等效的
class Point { var x: Int = 0 var y: Int = 0 } 等效于 class Point { var x: Int var y: Int init() { x = 0 y = 0 } } let p1 = Point()
- 下面这两种写法是完全等效的
2. 结构体与类的本质区别
- 结构体是
值类型
(枚举也是值类型),类是引用类型
(指针类型)
下面我们分析函数内的局部变量分别都在内存的什么位置
class Size {
var width = 1
var height = 2
}
struct Point {
var x: Int = 3
var y: Int = 4
}
func test() {
var size = Size()
var point = Point()
}
变量
size和point都是在栈空间- 不同的是
局部变量point
是一个结构体类型。结构体是值类型,结构体变量会在栈空间中分配内存,它里面的两个成员x、y按顺序的排布 - 而
局部指针变量size
是一个类的实例,类是引用类型,所以size指针
指向的已初始化的变量的存储空间,是在堆中分配的,size指针内部存储的是Size类的实例内存地址
3. 分析类的内存布局
-
- 我们先来看一下类的占用内存大小是多少
class Size { var width = 1 var height = 2 } print(MemoryLayout<Size>.stride) // 8
通过打印我们可以发现
MemoryLayout
获取的8个字节实际上是指针变量占用多少存储空间,并不是对象在堆中的占用大小 -
- 然后我们再看类的内存布局是怎样的
var size = Size() print(Mems.ptr(ofVal: &size)) // 0x000000010000c388 print(Mems.memStr(ofVal: &size)) // 0x000000010072dba0
通过打印我们可以看到变量里面存储的值也是一个地址
-
- 我们再打印该变量所指向的对象的内存布局是什么
print(Mems.size(ofRef: size)) // 32 print(Mems.ptr(ofRef: size)) // 0x000000010072dba0 print(Mems.memStr(ofRef: size)) // 0x000000010000c278 0x0000000200000003 0x0000000000000001 0x0000000000000002
通过打印可以看到在
堆中存储的对象的地址
和上面的指针变量里存储的值
是一样的内存布局里一共占用32个字节:
- 前16个字节分别用来存储一些
类信息
和引用计数
- 后面16个字节存储着类的成员变量的值
下面我们再从反汇编的角度来分析
- 我们要想确定类是否在堆空间中分配空间,通过反汇编来查看是否有调用
malloc函数
- 然后就一直跟进直到这里最好调用了
swift_slowAlloc
- 发现函数内部调用了系统的
malloc
在堆空间分配内存
注意:
结构体
和枚举
存储在哪里取决于它们是在哪里分配
的- 如果是在
函数中
分配的那就是在栈
里 - 如果是在
全局中
分配的那就是在数据段
- 如果是在
- 而
类
无论是在哪里分配的,对象都是在堆空间
中- 指向对象内存的指针的存储位置是不确定的,可能在栈中也可能在数据段
我们再看下面的
类型
占用内存大小是多少
class Size {
var width: Int = 0
var height: Int = 0
var test = true
}
let s = Size()
print(Mems.size(ofRef: s)) // 48
- 在
Mac、iOS
中的malloc函数
分配的内存大小总是16的倍数
- 类最前面会有
16个字节
用来存储类的信息
和引用计数
,所以实际占用内存是33个字节
,但由于malloc函数
分配的内存都是刚好大于或等于其所需内存的16最小倍数,所以分配48个字节
- 我们还可以通过
class_getInstanceSize
函数来获取类对象的内存大小
// 获取的是经过内存对齐后的内存大小,不是malloc函数分配的内存大小
print(class_getInstanceSize(type(of: s))) // 40
print(class_getInstanceSize(Size.self)) // 40
五、枚举
1. 枚举的基本用法
enum Direction {
case north
case south
case east
case west
}
// 简便写法
enum Direction {
case north, south, east, west
}
var dir = Direction.west
dir = Direction.east
dir = .north
print(dir) // north
switch dir {
case .north:
print("north")
case .south:
print("south")
case .east:
print("east")
case .west:
print("west")
}
2. 关联值(Associated Values)
有时会将枚举的成员值
和其他类型的值
关联 存储在一起
,会非常有用
enum Score {
case points(Int)
case grade(Character)
}
var score = Score.points(96)
score = .grade("A")
switch score {
case let .points(i):
debugPrint(i)
case let .grade(i):
debugPrint(i)
}
enum Date {
case digit(year: Int, month: Int, day: Int)
case string(String)
}
var date = Date.digit(year: 2020, month: 12, day: 5)
date = .string("2022-07-10")
//必要时【let】也可以改为【var】
switch date {
case .digit(let year, let month, let day):
debugPrint(year, month, day)
case let .string(value):
debugPrint(value)
}
关联值举例
![]()
enum Password {
case number(Int, Int, Int, Int)
case gesture(String)
}
var pwd = Password.number(5, 6, 4, 7)
pwd = .gesture("12369")
switch pwd {
case let .number(n1, n2, n3, n4):
print("number is ", n1, n2, n3, n4)
case let .gesture(str):
print("gesture is ", str)
}
必要时,使用了枚举关联值的switch-case
语句 里面的 let
也可以改成var
3. 原始值(Raw Values)
枚举成员可以使用相同类型
的默认值预先关联,这个默认值叫做原始值
enum PokerSuit: String {
case spade = "♠"
case heart = "♥"
case diamond = "♦"
case club = "♣"
}
let suit = PokerSuit.heart
debugPrint(suit)// heart
debugPrint(suit.rawValue)// ♥
debugPrint(PokerSuit.spade.rawValue)// ♠
enum Grade : String {
case perfect = "A"
case great = "B"
case good = "C"
case bad = "D"
}
print(Grade.perfect.rawValue) // A
print(Grade.great.rawValue) // B
print(Grade.good.rawValue) // C
print(Grade.bad.rawValue) // D
注意:
- 原始值不占用枚举变量的内存
- 原始值只是关联上了枚举变量,所以原始值占用内存的大小并不是枚举变量的大小
- 底层实现是通过计算属性/函数来获取原始值的
4. 隐式原始值(Implicitly Assigned Raw Values)
如果枚举的原始值类型是Int
、String
,Swift会自动分配原始值
字符串默认分配的原始值就是其变量名
enum Direction: String {
case north = "north"
case south = "south"
case east = "east"
case west = "west"
}
// 等价于上面
enum Direction: String {
case north, south, east, west
}
print(Direction.north) // north
print(Direction.north.rawValue) // north
Int类型
默认分配的原始值是从0开始递增的数字
enum Season: Int {
case spring, summer, autumn, winter
}
print(Season.spring.rawValue) // 0
print(Season.summer.rawValue) // 1
print(Season.autumn.rawValue) // 2
print(Season.winter.rawValue) // 3
如果有指定原始值的,下一个就会按照已经指定的值递增分配
enum Season: Int {
case spring = 1, summer, autumn = 4, winter
}
print(Season.spring.rawValue) // 1
print(Season.summer.rawValue) // 2
print(Season.autumn.rawValue) // 4
print(Season.winter.rawValue) // 5
5. 递归枚举(Recursive Enumeration)
-
- 递归枚举要用
indirect
关键字来修饰enum
,不然会报错
indirect enum ArithExpr { case number(Int) case sum(ArithExpr, ArithExpr) case difference(ArithExpr, ArithExpr) } 或者 enum ArithExpr { case number(Int) indirect case sum(ArithExpr, ArithExpr) indirect case difference(ArithExpr, ArithExpr) } let five = ArithExpr.number(5) let four = ArithExpr.number(4) let sum = ArithExpr.sum(five, four) let two = ArithExpr.number(2) let difference = ArithExpr.difference(sum, two) func calculate(_ expr: ArithExpr) -> Int { switch expr { case let .number(value): return value case let .sum(left, right): return calculate(left) + calculate(right) case let .difference(left, right): return calculate(left) - calculate(right) } } calculate(difference)
- 递归枚举要用
6. MemoryLayout
-
- 可以使用
MemoryLayout
获取数据类型占用的内存大小
64bit
的Int类型
占8个字节
let age = 10 MemoryLayout<Int>.stride // 8, 分配占用的空间大小 MemoryLayout<Int>.size // 8, 实际用到的空间大小 MemoryLayout<Int>.alignment // 8, 内存对齐参数 等同于 MemoryLayout.size(ofValue: age) MemoryLayout.stride(ofValue: age) MemoryLayout.alignment(ofValue: age)
- 可以使用
关联值和原始值的区别:
-
关联值类型会存储到枚举变量里面
-
原始值因为一开始就会知道默认值是多少,所以只做记录,不会存储
enum Password { case number(Int, Int, Int, Int) case other } MemoryLayout<Password>.stride // 40,分配占用的空间大小 MemoryLayout<Password>.size // 33,实际用到的空间大小 MemoryLayout<Password>.alignment // 8,对齐参数
enum Session: Int { case spring, summer, autnmn, winter } MemoryLayout<Session>.stride // 1,分配占用的空间大小 MemoryLayout<Session>.size // 1,实际用到的空间大小 MemoryLayout<Session>.alignment // 1,对齐参数
思考下面枚举变量的内存布局: 案例1:
enum TestEnum {
case test1, test2, test3
}
var t = TestEnum.test1
t = .test2
t = .test3
MemoryLayout<TestEnum>.stride // 1,分配占用的空间大小
MemoryLayout<TestEnum>.size // 1,实际用到的空间大小
MemoryLayout<TestEnum>.alignment // 1,对齐参数
案例2:
enum TestEnum : Int {
case test1 = 1, test2 = 2, test3 = 3
}
var t = TestEnum.test1
t = .test2
t = .test3
MemoryLayout<TestEnum>.stride // 1,分配占用的空间大小
MemoryLayout<TestEnum>.size // 1,实际用到的空间大小
MemoryLayout<TestEnum>.alignment // 1,对齐参数
案例3:
enum TestEnum {
case test1(Int, Int, Int)
case test2(Int, Int)
case test3(Int)
case test4(Bool)
case test5
}
var e = TestEnum.test1(1, 2, 3)
e = .test2(4, 5)
e = .test3(6)
e = .test4(true)
e = .test5
MemoryLayout<TestEnum>.stride // 32,分配占用的空间大小
MemoryLayout<TestEnum>.size // 25,实际用到的空间大小
MemoryLayout<TestEnum>.alignment // 8,对齐参数
案例4:
//注意!!!! 枚举选项只有一个,所以实际用到的内存空间 为0,但是要存储一个成员值 所以对其参数为1,给其分配一个字节
enum TestEnum {
case test
}
var t = TestEnum.test
MemoryLayout<TestEnum>.stride // 1,分配占用的空间大小
MemoryLayout<TestEnum>.size // 0,实际用到的空间大小
MemoryLayout<TestEnum>.alignment // 1,对齐参数
案例5:
enum TestEnum {
case test(Int)
}
var t = TestEnum.test(10)
MemoryLayout<TestEnum>.stride // 8,分配占用的空间大小
MemoryLayout<TestEnum>.size // 8,实际用到的空间大小
MemoryLayout<TestEnum>.alignment // 8,对齐参数
案例6:
enum TestEnum {
case test(Int)
}
var t = TestEnum.test(10)
MemoryLayout<TestEnum>.stride // 8,分配占用的空间大小
MemoryLayout<TestEnum>.size // 8,实际用到的空间大小
MemoryLayout<TestEnum>.alignment // 8,对齐参数
案例7:
enum TestEnum {
case test0
case test1
case test2
case test4(Int)
case test5(Int, Int)
case test6(Int, Int, Int, Bool)
}
var t = TestEnum.test(10)
MemoryLayout<TestEnum>.stride // 32,分配占用的空间大小
MemoryLayout<TestEnum>.size // 25,实际用到的空间大小
MemoryLayout<TestEnum>.alignment // 8,对齐参数
案例8:
enum TestEnum {
case test0
case test1
case test2
case test4(Int)
case test5(Int, Int)
case test6(Int, Int, Bool, Int)
}
var t = TestEnum.test(10)
MemoryLayout<TestEnum>.stride // 32,分配占用的空间大小
MemoryLayout<TestEnum>.size // 32,实际用到的空间大小
MemoryLayout<TestEnum>.alignment // 8,对齐参数
案例9:
enum TestEnum {
case test0
case test1
case test2
case test4(Int)
case test5(Int, Int)
case test6(Int, Bool, Int)
}
var t = TestEnum.test(10)
MemoryLayout<TestEnum>.stride //32,分配占用的空间大小
MemoryLayout<TestEnum>.size //25,实际用到的空间大小
MemoryLayout<TestEnum>.alignment //8,对齐参数
7. 枚举变量的内存布局
我们知道通过MemoryLayout
可以获取到枚举占用内存的大小,那枚举变量分别占用多少内存呢?
要想知道枚举变量的大小,我们需要通过查看枚举变量的内存布局来进行分析
枚举变量的分析准备
我们可以通过Xcode
里的View Memory
选项来查看详细的内存布局
- 可以在运行程序时,通过控制台打印的枚举变量右键选择
View Memory of *
进入到内存布局的页面 - 还可以从
Xcode
标题栏中选择Debug -> Debug Workflow -> View Memory
进入到内存布局的页面 - 进入到该页面,然后通过输入变量的内存地址来查看
- 我们可以下载一个小工具来获取到变量的内存地址
下载地址:github.com/CoderMJLee/… - 然后将下载好的这个文件
Mems.swift
拖到自己的Xcode
中 调用这个函数就可以了
print(Mems.ptr(ofVal: &t))
我们来分析下面的枚举变量的情况
enum TestEnum {
case test1, test2, test3
}
var t = TestEnum.test1
print(Mems.ptr(ofVal: &t))
t = TestEnum.test2
t = TestEnum.test3
print(MemoryLayout<TestEnum>.stride) // 1
print(MemoryLayout<TestEnum>.size) // 1
print(MemoryLayout<TestEnum>.alignment) // 1
分别将断点打在给枚举变量t
赋值的三句代码上,然后运行程序观察每次断点之后的内存布局有什么变化
通过上图可以看到,每个枚举变量都分配了一个字节的大小,并且存储的值分别是0、1、2,我们可以知道这一个字节的大小就是用来存储枚举成员值
的
我们再来分析一个枚举变量的情况
enum TestEnum: Int {
case test1 = 1, test2 = 2, test3 = 3
}
var t = TestEnum.test1
print(Mems.ptr(ofVal: &t))
t = TestEnum.test2
t = TestEnum.test3
print(MemoryLayout<TestEnum>.stride) // 1
print(MemoryLayout<TestEnum>.size) // 1
print(MemoryLayout<TestEnum>.alignment) // 1
通过上图可以看到,每个枚举变量存储的值也是0、1、2,并且分配了一个字节的大小
可以证明枚举变量的内存大小和原始值类型无关,而且枚举变量里存储的值和原始值也无关
我们再来分析一个枚举变量的情况
enum TestEnum {
case test1(Int, Int, Int) // 24
case test2(Int, Int) // 16
case test3(Int) // 8
case test4(Bool) // 1
case test5 // 1
}
var t = TestEnum.test1(1, 2, 3)
print(Mems.ptr(ofVal: &t))
t = TestEnum.test2(4, 5)
t = TestEnum.test3(6)
t = TestEnum.test4(true)
t = TestEnum.test5
MemoryLayout<TestEnum>.size // 25
MemoryLayout<TestEnum>.stride // 32
MemoryLayout<TestEnum>.alignment // 8
我们先通过打印了解到枚举类型总共分配了32个字节
,然后我们通过断点分别来观察枚举变量的内存布局
执行完第一句我们可以看到,前面24个字节分别用来存储关联值1、2、3,第25个字节用来存储成员值0,之所以分配32个字节是因为内存对齐的原因
// 调整排版后的内存布局如下所示
01 00 00 00 00 00 00 00
02 00 00 00 00 00 00 00
03 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
执行完第二句我们可以看到,前面16个字节分半用来存储关联值4、5,然后第25个字节用来存储成员值1
// 调整排版后的内存布局如下所示
04 00 00 00 00 00 00 00
05 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
01 00 00 00 00 00 00 00
执行完第三句我们可以看到,前面8个字节分半用来存储关联值6,然后第25个字节用来存储成员值2
// 调整排版后的内存布局如下所示
06 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
02 00 00 00 00 00 00 00
执行完第四句我们可以看到,由于是Bool类型,那么只用了第一个字节来存储关联值1,然后第25个字节用来存储成员值3
// 调整排版后的内存布局如下所示
01 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
03 00 00 00 00 00 00 00
执行完最后一句我们可以看到,由于没有关联值,那么只用了第25个字节存储成员值4
// 调整排版后的内存布局如下所示
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
04 00 00 00 00 00 00 00
总结:内存分配情况:一个字节存储成员值,n个字节存储关联值(n取占用内存最大的关联值),任何一个case的关联值都共有这n个字节
我们再来看几个情况
enum TestEnum {
case test
}
MemoryLayout<Session>.stride // 1,分配占用的空间大小
MemoryLayout<Session>.size // 0,实际用到的空间大小
MemoryLayout<Session>.alignment // 1,对齐参数
如果枚举里只有一个case
,那么实际用到的空间为0,都不用特别分配内存来进行存储
enum TestEnum {
case test(Int)
}
MemoryLayout<Session>.stride // 8,分配占用的空间大小
MemoryLayout<Session>.size // 8,实际用到的空间大小
MemoryLayout<Session>.alignment // 8,对齐参数
可以看到分配的内存大小就是关联值类型决定的,因为只有一个case
,所以都不需要再额外分配内存来存储是哪个case
了
六、值类型和引用类型
1. 值类型
- 值类型赋值给
var、let或者给函数
传参,是直接将所有内容拷贝一份
- 类似于对文件进行
copy、paste
操作,产生了全新的文件副本,属于深拷贝(deep copy)
值类型进行拷贝的内存布局如下所示
struct Point {
var x: Int = 3
var y: Int = 4
}
func test() {
var p1 = Point(x: 10, y: 20)
var p2 = p1
p2.x = 4
p2.y = 5
print(p1.x, p1.y)
}
test()
我们通过反汇编来进行分析
通过上述分析可以发现,值类型的赋值内部会先将p1的成员值保存起来,再给p2进行赋值,所以不会影响到p1
2. 值类型的赋值操作
- 在Swift标准库中,为了提升性能,
Array、String、Dictionary、Set
采用了Copy On Write
的技术 - 如果
只是将赋值操作
,那么只会进行浅拷贝
,两个变量使用的还是同一块存储空间 - 只有当
进行了”写“的操作
时,才会进行深拷贝
操作 - 对于标准库值类型的赋值操作,Swift能确保最佳性能,所以没必要为了保证最佳性能来避免赋值
- 建议:不需要修改值的,尽量定义成
let
var s1 = "Jack"
var s2 = s1
s2.append("_Rose")
print(s1) // Jack
print(s2) // Jack_Rose
var a1 = [1, 2, 3]
var a2 = a1
a2.append(4)
a1[0] = 2
print(a1) // [2, 2, 3]
print(a2) // [1, 2, 3, 4]
var d1 = ["max" : 10, "min" : 2]
var d2 = d1
d1["other"] = 7
d2["max"] = 12
print(d1) // ["other" : 7, "max" : 10, "min" : 2]
print(d2) // ["max" : 12, "min" : 2]
我们再看下面这段代码
对于p1来说,再次赋值也只是覆盖了成员x、y
的值而已,都是同一个结构体变量
struct Point {
var x: Int
var y: Int
}
var p1 = Point(x: 10, y: 20)
p1 = Point(x: 11, y: 22)
用let定义的赋值操作
- 如果用
let
定义的常量赋值结构体类型会报错,并且修改结构体里的成员值也会报错 - 用
let
定义就意味着常量里存储的值不可更改,而结构体是由x和y这16个字节组成的,所以更改x和y就意味着结构体的值要被覆盖,所以报错
3. 引用类型
- 引用赋值给
var、let或者给函数
传参,是将内存地址拷贝一份
- 类似于制作一个文件的替身(快捷方式、链接),指向的是同一个文件,属于
浅拷贝(shallow copy)
class Size { var width = 0 var height = 0 init(width: Int, height: Int) { self.width = width self.height = height } } func test() { var s1 = Size(width: 10, height: 20) var s2 = s1 s2.width = 11 s2.height = 22 print(s1.height, s1.width) } test()
由于s1和s2都指向的同一块存储空间,所以s2修改了成员变量,s1再调用成员变量也已经是改变后的了
我们通过反汇编来进行分析
![]()
![]()
- 堆空间分配完内存之后,我们拿到
rax
的值查看内存布局 - 发现
rax
里和对象的结构一样,证明rax
里存储的就是对象的地址 - 将新的值11和22分别覆盖掉堆空间对象的成员值
通过上面的分析可以发现,修改的成员值都是改的同一个地址的对象,所以修改了p2的成员值,会影响到p1
4. 对象的堆空间申请过程
- 在Swift中,创建类的实例对象,要向堆空间申请内存,大概流程如下
Class.__allocating_init()
- libswiftCore.dylib:
_swift_allocObject_
- libswiftCore.dylib:
swift_slowAlloc
- libsystem_malloc.dylib:
malloc
- 在Mac、iOS中的
malloc
函数分配的内存大小总是16的倍数 - 通过
class_getInstanceSize
可以得知:类的对象至少需要占用多少内存class Point{ var x = 11 var test = true var y = 22 } var p = Point() class_getInstanceSize(type(of: p)) // 40 class_getInstanceSize(Point.self) // 40
5. 引用类型的赋值操作
-
- 将引用类型初始化对象赋值给同一个指针变量,指针变量会指向另一块存储空间
class Size { var width: Int var height: Int init(width: Int, height: Int) { self.width = width self.height = height } } var s1 = Size(width: 10, height: 20) s1 = Size(width: 11, height: 22)
用let定义的赋值操作
-
- 如果用
let
定义的常量赋值引用类型会报错,因为会改变指针常量里存储的8个字节的地址值
- 如果用
-
- 但修改类里的属性值不会报错,因为修改属性值并不是修改的指针常量的内存,只是通过指针常量找到类所存储的堆空间的内存地址去修改类的属性
- 但修改类里的属性值不会报错,因为修改属性值并不是修改的指针常量的内存,只是通过指针常量找到类所存储的堆空间的内存地址去修改类的属性
6. 嵌套类型
struct Poker {
enum Suit: Character {
case spades = "♠️",
hearts = "♥️",
diamonds = "♦️",
clubs = "♣️"
}
enum Rank: Int {
case two = 2, three, four, five, six, seven, eight, nine, ten
case jack, queen, king, ace
}
}
print(Poker.Suit.hearts.rawValue)
var suit = Poker.Suit.spades
suit = .diamonds
var rank = Poker.Rank.five
rank = .king
7. 枚举、结构体、类都可以定义方法
-
- 一般把定义在枚举、结构体、类内部的函数,叫做方法
struct Point { var x: Int = 10 var y: Int = 10 func show() { print("show") } } let p = Point() p.show()
class Size { var width: Int = 10 var height: Int = 10 func show() { print("show") } } let s = Size() s.show()
enum PokerFace: Character { case spades = "♠️", hearts = "♥️", diamonds = "♦️", clubs = "♣️" func show() { print("show") } } let pf = PokerFace.hearts pf.show()
-
- 方法不管放在哪里,其内存都是放在代码段中
-
- 枚举、结构体、类里的方法其实会有隐式参数
class Size { var width: Int = 10 var height: Int = 10 // 默认会有隐式参数,该参数类型为当前枚举、结构体、类 func show(self: Size) { print(self.width, self.height) } }
专题系列文章
1. 前知识
- 01-探究iOS底层原理|综述
- 02-探究iOS底层原理|编译器LLVM项目【Clang、SwiftC、优化器、LLVM】
- 03-探究iOS底层原理|LLDB
- 04-探究iOS底层原理|ARM64汇编
2. 基于OC语言探索iOS底层原理
- 05-探究iOS底层原理|OC的本质
- 06-探究iOS底层原理|OC对象的本质
- 07-探究iOS底层原理|几种OC对象【实例对象、类对象、元类】、对象的isa指针、superclass、对象的方法调用、Class的底层本质
- 08-探究iOS底层原理|Category底层结构、App启动时Class与Category装载过程、load 和 initialize 执行、关联对象
- 09-探究iOS底层原理|KVO
- 10-探究iOS底层原理|KVC
- 11-探究iOS底层原理|探索Block的本质|【Block的数据类型(本质)与内存布局、变量捕获、Block的种类、内存管理、Block的修饰符、循环引用】
- 12-探究iOS底层原理|Runtime1【isa详解、class的结构、方法缓存cache_t】
- 13-探究iOS底层原理|Runtime2【消息处理(发送、转发)&&动态方法解析、super的本质】
- 14-探究iOS底层原理|Runtime3【Runtime的相关应用】
- 15-探究iOS底层原理|RunLoop【两种RunloopMode、RunLoopMode中的Source0、Source1、Timer、Observer】
- 16-探究iOS底层原理|RunLoop的应用
- 17-探究iOS底层原理|多线程技术的底层原理【GCD源码分析1:主队列、串行队列&&并行队列、全局并发队列】
- 18-探究iOS底层原理|多线程技术【GCD源码分析1:dispatch_get_global_queue与dispatch_(a)sync、单例、线程死锁】
- 19-探究iOS底层原理|多线程技术【GCD源码分析2:栅栏函数dispatch_barrier_(a)sync、信号量dispatch_semaphore】
- 20-探究iOS底层原理|多线程技术【GCD源码分析3:线程调度组dispatch_group、事件源dispatch Source】
- 21-探究iOS底层原理|多线程技术【线程锁:自旋锁、互斥锁、递归锁】
- 22-探究iOS底层原理|多线程技术【原子锁atomic、gcd Timer、NSTimer、CADisplayLink】
- 23-探究iOS底层原理|内存管理【Mach-O文件、Tagged Pointer、对象的内存管理、copy、引用计数、weak指针、autorelease
3. 基于Swift语言探索iOS底层原理
关于函数
、枚举
、可选项
、结构体
、类
、闭包
、属性
、方法
、swift多态原理
、String
、Array
、Dictionary
、引用计数
、MetaData
等Swift基本语法和相关的底层原理文章有如下几篇:
- 01-📝Swift5常用核心语法|了解Swift【Swift简介、Swift的版本、Swift编译原理】
- 02-📝Swift5常用核心语法|基础语法【Playground、常量与变量、常见数据类型、字面量、元组、流程控制、函数、枚举、可选项、guard语句、区间】
- 03-📝Swift5常用核心语法|面向对象【闭包、结构体、类、枚举】
- 04-📝Swift5常用核心语法|面向对象【属性、inout、类型属性、单例模式、方法、下标、继承、初始化】
- 05-📝Swift5常用核心语法|高级语法【可选链、协议、错误处理、泛型、String与Array、高级运算符、扩展、访问控制、内存管理、字面量、模式匹配】
- 06-📝Swift5常用核心语法|编程范式与Swift源码【从OC到Swift、函数式编程、面向协议编程、响应式编程、Swift源码分析】
其它底层原理专题
1. 底层原理相关专题
2. iOS相关专题
- 01-iOS底层原理|iOS的各个渲染框架以及iOS图层渲染原理
- 02-iOS底层原理|iOS动画渲染原理
- 03-iOS底层原理|iOS OffScreen Rendering 离屏渲染原理
- 04-iOS底层原理|因CPU、GPU资源消耗导致卡顿的原因和解决方案