【iOS】Swift系列二十 - String和Array底层分析

StringArray用起来很简单,但底层是如何存储的呢?

一、String

1.1. 1个String变量占用多少内存?

示例代码:

1
2
3
4
5
6
7
8
9
10
var str1 = "0123456789"
print(MemoryLayout.size(ofValue: str1))
// 输出:16

print(Mems.memStr(ofVal: &str1))
/*
输出:
0x3736353433323130
0xea00000000003938
*/

结论: 一个String变量占用16个字节。

这16个字节是如何存储的呢?

1.2. 汇编分析

变量str1的前8个字节和后8个字节分别存储的是什么呢?其实存储的都是ASCII码值。


0的ASCII码值对应的十六进制是0x30

前8个字节0x3736353433323130,分离后:

1
0x37 36 35 34 33 32 31 30

后8个字节0xea00000000003938分离后:

1
0xea0000000000 39 38

0xea代表什么意思?
这里应该分开来看,0xe a,后面的一位a代表字符串长度(即长度是10),前面的e代表字符串标识(e代表字符串内存存储在变量里面的)。

那也就意味着字符串长度最大是f,这样就能完整的填满内存。

1.3. 长度为15位的字符串

示例代码:

1
2
3
4
5
6
7
var str1 = "0123456789ABCDE"
print(Mems.memStr(ofVal: &str1))
/*
输出:
0x3736353433323130
0xef45444342413938
*/

15位长度字符串,位数已经用f表示了。这种情况类似于OC中的tagger pointer(字符串的内容直接放到对象str1内存里面了)。

如果再多1位会发生什么变化呢?

1.4. 长度超过15位的字符串

示例代码:

1
2
3
4
5
6
7
8
9
var str1 = "0123456789ABCDEF"
print(MemoryLayout.size(ofValue: str1))
// 输出:16
print(Mems.memStr(ofVal: &str1))
/*
输出:
0xd000000000000010
0x80000001000075f0
*/

这次输出明显和之前的不一样。即使长度继续加大,内存变化也不大,这也意味着字符串确实没有存储在这16个字节内。

1.4.1. 字符串内存地址

第一步: 查看字符串初始化入参

  • 字符串真实地址:rax = 0x100001cc2 + 0x594e = 0x100007610
  • rdi存放着字符串的真实地址0x100007610
  • rsi存放的是字符串的长度0x10

第二步: 进入String.init函数

  • rsi(存放的是0x10)和0xf进行比较(长度比较)
    • 如果字符串长度小于16位,直接进入jle指定的地址(内存直接存放到内存)
    • 否则,继续往下执行
  • 函数返回地址:rax = rdi(0x100007610) + 0x7fffffffffffffe0 = 0x80000001000075F0

第三步: 函数返回值

  • String.init函数把返回值rdx给了字符串内存的后8个字节
  • 字符串长度给了字符串的前8个字节

第四步: 查看真实地址存放的内容

  • 这时候再看示例代码的内存输出结果,就会豁然开朗了

扩展:字符串真实地址rax = rdi(0x100007610) + 0x7fffffffffffffe0 = 0x80000001000075F0等价于rax = 0x00000001000075F0 + 0x20

1.4.2. 字符串存放区域

字符串的真实地址是存放在内存中的哪块区域呢?

其实根据经验可以看出上面示例代码中字符串存放在常量区,但是根据汇编代码又感觉是放在全局区。

我们可以通过Mach-O文件查看字符串存放在哪块区域(程序文件在是实际运行中内存可能是动态的,但本案例首次运行也足以能够证明字符串存放的区域)。

可以看到,实例中的字符串是存放在常量区的(cstring)。字符串的后8个字节保存的地址指向00007610这块内存。

注意:Mac平台的Mach-O文件呈现出来的地址需要加上偏移量0x100000000(即虚拟内存地址),例如:字符串0x100007610Mach-O中查找00007610的地址即可。

1.5. 字符串拼接操作引起的内存变化

1.5.1. 长度不超过15位的字符串

1
2
3
4
var str1 = "0123456789"
print(Mems.memStr(ofVal: &str1)) // 输出:0x3736353433323130 0xea00000000003938
str1.append("A")
print(Mems.memStr(ofVal: &str1)) // 输出:0x3736353433323130 0xeb00000000413938

通过输出的内存地址可知,当字符串内容发生改变,但长度不超过15位时,字符串内容依然是直接放到内存里面的。

1.5.2. 长度超过15位的字符串

1
2
3
4
var str1 = "0123456789ABCDEF"
print(Mems.memStr(ofVal: &str1)) // 输出:0xd000000000000010 0x80000001000075e0
str1.append("G")
print(Mems.memStr(ofVal: &str1)) // 输出:0xf000000000000011 0x000000010043f8a0

字符串是存放到常量区的,也就意味着内存是不允许修改的。所以修改字符串内容时(超过15位),会在堆空间重新开辟一块内存来存储内容。

总结:

  • 字符串长度不超过15位,内容直接放到变量内存中
  • 长度超过15位,字符串内容存放在常量区
    • 内存中的后8个字节存放的是字符串内容真实存放的内存地址
    • 前8个字节存放的是字符串长度及标识符(标识符用来标记字符串存放到哪个区域)
  • 只要长度超过15位拼接字符串,都会重新开辟堆空间存放字符串内容。

二、Array

官方定义的数组是结构体(值类型):

1
public struct Array<Element>

2.1. 1个Array变量占用多少内存?

结构体的内存占用大小是把存放到结构体中的变量占用内存大小加起来。

示例代码一:

1
2
3
4
5
6
struct Point {
var x = 0, y = 0
}
var p = Point()
print(MemoryLayout.stride(ofValue: p))
// 输出:16

上面示例代码一中结构体一共占用16个字节内存。

数组也是结构体,占用内存大小的计算方法是否和上面的示例代码一致呢?

示例代码二:

1
2
3
var arr = [1, 2, 3, 4]
print(MemoryLayout.stride(ofValue: arr))
// 输出:8

很遗憾,只占用8个字节内存,且是Int类型占用的内存大小。那么数组里面的内容是存放在哪里呢?

2.2. 数组中的数据存放在哪里?

通过汇编分析可以知道,数组中数组是存放在堆空间的,数组变量内存存放着堆空间的数组对象地址。

示例代码一:

1
2
3
4
5
6
7
8
9
10
11
12
13
var arr = [1, 2, 3, 4]
print(Mems.memStr(ofRef: arr))
/*
输出:
0x00007fff8e5f54d8
0x0000000200000002
0x0000000000000004
0x0000000000000008
0x0000000000000001
0x0000000000000002
0x0000000000000003
0x0000000000000004
*/

通过内存布局看到,数组内容需要跳过前面的32个字节。那么前面的字节分别存放着什么东西呢?

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

数组的容量会自动扩容至元素个数的两倍,且是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
var arr = [1, 2, 3, 4]
print(Mems.memStr(ofRef: arr))
/*
输出:
0x00007fff8e5f54d8
0x0000000200000002
0x0000000000000004
0x0000000000000008
0x0000000000000001 0x0000000000000002 0x0000000000000003 0x0000000000000004
*/

arr.append(5)
print(Mems.memStr(ofRef: arr))
/*
输出:
0x00007fff8e5f54d8
0x0000000200000002
0x0000000000000005
0x0000000000000010
0x0000000000000001 0x0000000000000002 0x0000000000000003 0x0000000000000004
0x0000000000000005 0x0000000000000000 0x0000000000000000 0x0000000000000000
*/
arr.append(6)
arr.append(7)
arr.append(8)
arr.append(9)
print(Mems.memStr(ofRef: arr))
/*
输出:
0x00007fff8e5f54d8
0x0000000200000002
0x0000000000000009
0x0000000000000020
0x0000000000000001 0x0000000000000002 0x0000000000000003 0x0000000000000004
0x0000000000000005 0x0000000000000006 0x0000000000000007 0x0000000000000008
0x0000000000000009 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000
*/

所以,数组的表象是结构体,但其本质是引用类型。