KM的博客.

Swift内存探究之值类型

字数统计: 1.7k阅读时长: 7 min
2022/03/30

Swift值类型内存探究

enum

默认关联值类型的枚举:

枚举类型默认对齐字节是 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 没有关联值 占用内存 1 字节
enum Test {
case t1,t2,t3
}
print(MemoryLayout<Test>.alignment)//枚举类型内存对齐:1
print(MemoryLayout<Test>.size)//枚举类型实际大小:1
print(MemoryLayout<Test>.stride)//枚举类型分配:1
// 有默认关联值 占用内存 1 字节
enum TestWithVale: Int {
case t1 = 1
case t2 = 3
case t3 = 5
}
print(MemoryLayout<TestWithVale>.alignment)//枚举类型内存对齐:1
print(MemoryLayout<TestWithVale>.size)//枚举类型实际大小:1
print(MemoryLayout<TestWithVale>.stride)//枚举类型分配:1
关联值为元组类型的枚举

枚举中包含 Int 类型的值,内存对齐以 Int 为准,内存对齐是 8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 实际内存占用 17 系统内存分配 24
enum Password {
case num(Int, Int)
case str
}
print(MemoryLayout<Password>.alignment)//Int类型内存对齐:8
print(MemoryLayout<Password>.size)//实际大小:16 + 1
print(MemoryLayout<Password>.stride)//系统分配:16 + 8
// 实际内存占用 25 系统内存分配 32
// 多个元组类型的枚举值:以元组规模最大的为内存分配依据
enum TestEnum {
case t1(Int, Int, Int)
case t2(Int, Int)
case t3(Int)
case t4(Bool)
}
print(MemoryLayout<TestEnum>.alignment)//Int类型内存对齐:8
print(MemoryLayout<TestEnum>.size)//实际大小:24 + 1
print(MemoryLayout<TestEnum>.stride)//系统分配:24 + 8

问题实际分配内存为何是25?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
///带有关联值的枚举
enum TestEnum {
case t1(Int, Int, Int)
case t2(Int, Int)
case t3(Int)
case t4(Bool)
}
print(MemoryLayout<TestEnum>.alignment)//Int类型内存对齐:8
print(MemoryLayout<TestEnum>.size)//实际大小:24 + 1
print(MemoryLayout<TestEnum>.stride)//系统分配:24 + 8


///带有关联值的枚举
enum Password {
case num(Int, Int)
case str
}
print(MemoryLayout<Password>.alignment)//Int类型内存对齐:8
print(MemoryLayout<Password>.size)//实际大小:16 + 1
print(MemoryLayout<Password>.stride)//系统分配:16 + 8

Struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Point {
var isHit: Bool
}
let p = Point(isHit: true)
print(MemoryLayout<Point>.alignment)//Int类型内存对齐:1
print(MemoryLayout<Point>.size)//实际大小: 1
print(MemoryLayout<Point>.stride)//系统分配:1
print(MemoryLayout.stride(ofValue: p)) // 实例变量 p 内存是 1

struct Point {
let x: Int
}
print(MemoryLayout<Point>.alignment)//Int类型内存对齐:8
print(MemoryLayout<Point>.size)//实际大小: 8
print(MemoryLayout<Point>.stride)//系统分配:8
let p = Point(x: 10)
print(MemoryLayout.stride(ofValue: p)) // 实例变量 p 内存是 8

实际分配内存是多少?实际大小是多少? 17 24

1
2
3
4
5
6
7
8
9
10
///结构体占用内存 17,实际分配内存 16 + 8
struct Point {
let x: Int
let y: Int
var isHit: Bool
}

print(MemoryLayout<Point>.alignment)//Int类型内存对齐:8
print(MemoryLayout<Point>.size)//实际大小:16 + 1
print(MemoryLayout<Point>.stride)//系统分配:16 + 8

字符串 String

字符串变量内存占用 16 个字节,那字符串元素是如何存放的?

1
2
3
4
var str = "123"
print(MemoryLayout.stride(ofValue: str)) // 打印结果是 str占用内存16
// str内存分布
// 0x0000000000333231 0xe300000000000000 直接存储 123 的 ASII 码; 位数标记为 3

我们看到字符 123 直接存放在str内存中第 1个 8 字节中,位数 3 存放在 str 内存第2个 8 字节中

1
2
3
4
str.append("A") // A的 ASII 码是 41
// 再次验证 str内存分布
// 0x0000000041333231 0xe400000000000000
// 直接存储 123A 的 ASII 码; 位数标记为 4
当字符串长度为 15和 16 时分别会发生什么?
1
2
3
4
var str1 = "0123456789ABCDE"
// str1的长度是 15,此时刚好存储在 16 个字节当中
// 0x3736353433323130 0xef45444342413938 直接存储 0-9 A-E的 ASII 码
// 位数标记 f 为 15 位

如果我们插入第 16 个元素:

1
2
3
4
str1.append("F") 
// 此时字符串为长度 16
// 此时 str1 内存地址: 0xf000000000000010 0x00000001007c39c0

当字符串长度为16位时,再次查看str1的内存我们发现:

  • 这里内存地址不再是字符串的 ASII,内存中前 8 位字节存放字符串长度

数组Array

首先看一个问题: arr和 arr1 内存占用都是 8 个字节,那 arr 的元素存放在哪里呢?

1
2
3
4
var arr = [1, 2]
print(MemoryLayout.stride(ofValue: arr)) // arr 内存占用 8 字节
var arr1 = [1, 2, 3, 4,5,6,7,8,9,10]
print(MemoryLayout.stride(ofValue: arr1)) // arr1 内存占用 8 字节
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
38
39
40
41
42
43
44
var arr = [1, 2]
print(arr.count) // 2
print(arr.capacity) // 2
print(Mems.memStr(ofRef: arr))
//0x00007fff81600ef0 第1个 8 字节:存放着数组引用类型信息内存地址
//0x0000000200000003 第2个 8 字节: 数组的引用计数
//0x0000000000000002 第3个 8 字节: 数组的元素个数 2
//0x0000000000000004 第4个 8 字节: 数组预计扩容容量 4
//0x0000000000000001 第5个 8 字节: 数组元素 1
//0x0000000000000002 第6个 8 字节: 数组元素 2
arr.append(3)
arr.append(4)
arr.append(5)
arr.append(6)
arr.append(7)
arr.append(8)
arr.append(9)
arr.append(10)

print(arr.count) // 10
print(arr.capacity) // 16
print(Mems.memStr(ofRef: arr))
//0x00007fff81600ef0 第1个 8 字节:存放着数组引用类型信息内存地址
//0x0000000200000003 第2个 8 字节: 数组的引用计数
//0x0000000000000007 第3个 8 字节: 数组的元素个数10
//0x0000000000000020 第4个 8 字节: 数组预计扩容容量 20,实际扩容容量 16

//0x0000000000000001 第5个 8 字节: 数组元素 1
//0x0000000000000002 第6个 8 字节: 数组元素 2
//0x0000000000000003
//0x0000000000000004
//0x0000000000000005
//0x0000000000000006
//0x0000000000000007
//0x0000000000000008
//0x0000000000000009
//0x000000000000000a 第16个 8 字节: 数组元素 10
//0x0000000000000000
//0x0000000000000000
//0x0000000000000000
//0x0000000000000000
//0x0000000000000000
//0x0000000000000000 第22个 8 字节

注意通过内存布局看到,数组内容需要跳过前面的32个字节。

从第 5 个8字节开始才是数组元素中保存的变量:1…10

总结:

  • 第一段8个字节:存放着数组相关引用类型信息内存地址
  • 第二段8个字节:数组的引用计数
  • 第三段8个字节:数组的元素个数
  • 第四段8个字节:数组的容量
  • 后面依次存放着数组的元素

数组的容量会自动扩容至capacity的两倍

数组扩容原则: capacity * 2 (capacity是 2 的n次方: 1 2 4 8 16)

Array扩容capacity * 2

1
2
3
4
@inlinable
internal func _growArrayCapacity(_ capacity: Int) -> Int {
return capacity * 2
}

协议的内存管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protocol DragonFire {}
extension DragonFire {
func fire() {

}
}

struct YellowDragon: DragonFire {
let year = "8"
let teeth = 48
}

print(MemoryLayout<YellowDragon>.size) // 24
print(MemoryLayout<DragonFire>.size) // 40

协议类型内存管理使用Existential Container 内存模型。

前三个word使用Value buffer 来存储inline的值

第四个word使用Value Witness Table 存储各种操作如allocate、copy、destruct、deallocate等

第五个word使用Protocol Witness Table 来存储协议的函数

泛型的内存管理

泛型采用是和Exsistential Container原理类似的内存管理。

Value Witness Table 和 Protocol Witness Table是作为隐形参数传递到泛型方法里。

不过经过编辑器的层层inline优化后,最终类型会被推导出来,也就不再需要Existential Container 这一套方法了。

CATALOG
  1. 1. Swift值类型内存探究
  2. 2. enum
    1. 2.0.0.0.1. 默认关联值类型的枚举:
    2. 2.0.0.0.2. 关联值为元组类型的枚举
  • 3. Struct
    1. 3.0.1. 字符串 String
      1. 3.0.1.0.1. 当字符串长度为 15和 16 时分别会发生什么?
  • 3.0.2. 数组Array
    1. 3.0.2.1. 总结:
    2. 3.0.2.2. Array扩容capacity * 2
  • 4. 协议的内存管理
  • 5. 泛型的内存管理