String
和Array
用起来很简单,但底层是如何存储的呢?
String
变量占用多少内存?示例代码:
var str1 = "0123456789"
print(MemoryLayout.size(ofValue: str1))
// 输出:16
print(Mems.memStr(ofVal: &str1))
/*
输出:
0x3736353433323130
0xea00000000003938
*/
结论: 一个String
变量占用16个字节。
这16个字节是如何存储的呢?
变量str1
的前8个字节和后8个字节分别存储的是什么呢?其实存储的都是ASCII码值。
前8个字节0x3736353433323130
,分离后:
0x37 36 35 34 33 32 31 30
后8个字节0xea00000000003938
分离后:
0xea0000000000 39 38
0xea
代表什么意思?
这里应该分开来看,0xe a
,后面的一位a
代表字符串长度(即长度是10),前面的e
代表字符串标识(e
代表字符串内存存储在变量里面的)。
那也就意味着字符串长度最大是f
,这样就能完整的填满内存。
示例代码:
var str1 = "0123456789ABCDE"
print(Mems.memStr(ofVal: &str1))
/*
输出:
0x3736353433323130
0xef45444342413938
*/
15位长度字符串,位数已经用f
表示了。这种情况类似于OC
中的tagger pointer
(字符串的内容直接放到对象str1
内存里面了)。
如果再多1位会发生什么变化呢?
示例代码:
var str1 = "0123456789ABCDEF"
print(MemoryLayout.size(ofValue: str1))
// 输出:16
print(Mems.memStr(ofVal: &str1))
/*
输出:
0xd000000000000010
0x80000001000075f0
*/
这次输出明显和之前的不一样。即使长度继续加大,内存变化也不大,这也意味着字符串确实没有存储在这16个字节内。
rax = 0x100001cc2 + 0x594e = 0x100007610
rdi
存放着字符串的真实地址0x100007610
rsi
存放的是字符串的长度0x10
rsi
(存放的是0x10
)和0xf
进行比较(长度比较)
jle
指定的地址(内存直接存放到内存)rax = rdi(0x100007610) + 0x7fffffffffffffe0 = 0x80000001000075F0
String.init
函数把返回值rdx
给了字符串内存的后8个字节扩展:字符串真实地址
rax = rdi(0x100007610) + 0x7fffffffffffffe0 = 0x80000001000075F0
等价于rax = 0x00000001000075F0 + 0x20
。
其实根据经验可以看出上面示例代码中字符串存放在常量区,但是根据汇编代码又感觉是放在全局区。
我们可以通过Mach-O
文件查看字符串存放在哪块区域(程序文件在是实际运行中内存可能是动态的,但本案例首次运行也足以能够证明字符串存放的区域)。
可以看到,实例中的字符串是存放在常量区的(cstring
)。字符串的后8个字节保存的地址指向00007610
这块内存。
注意:
Mac
平台的Mach-O
文件呈现出来的地址需要加上偏移量0x100000000
(即虚拟内存地址),例如:字符串0x100007610
在Mach-O
中查找00007610
的地址即可。
var str1 = "0123456789"
print(Mems.memStr(ofVal: &str1)) // 输出:0x3736353433323130 0xea00000000003938
str1.append("A")
print(Mems.memStr(ofVal: &str1)) // 输出:0x3736353433323130 0xeb00000000413938
通过输出的内存地址可知,当字符串内容发生改变,但长度不超过15位时,字符串内容依然是直接放到内存里面的。
var str1 = "0123456789ABCDEF"
print(Mems.memStr(ofVal: &str1)) // 输出:0xd000000000000010 0x80000001000075e0
str1.append("G")
print(Mems.memStr(ofVal: &str1)) // 输出:0xf000000000000011 0x000000010043f8a0
字符串是存放到常量区的,也就意味着内存是不允许修改的。所以修改字符串内容时(超过15位),会在堆空间重新开辟一块内存来存储内容。
总结:
官方定义的数组是结构体(值类型):
public struct Array<Element>
结构体的内存占用大小是把存放到结构体中的变量占用内存大小加起来。
示例代码一:
struct Point {
var x = 0, y = 0
}
var p = Point()
print(MemoryLayout.stride(ofValue: p))
// 输出:16
上面示例代码一中结构体一共占用16个字节内存。
数组也是结构体,占用内存大小的计算方法是否和上面的示例代码一致呢?
示例代码二:
var arr = [1, 2, 3, 4]
print(MemoryLayout.stride(ofValue: arr))
// 输出:8
很遗憾,只占用8个字节内存,且是Int
类型占用的内存大小。那么数组里面的内容是存放在哪里呢?
通过汇编分析可以知道,数组中数组是存放在堆空间的,数组变量内存存放着堆空间的数组对象地址。
示例代码一:
var arr = [1, 2, 3, 4]
print(Mems.memStr(ofRef: arr))
/*
输出:
0x00007fff8e5f54d8
0x0000000200000002
0x0000000000000004
0x0000000000000008
0x0000000000000001
0x0000000000000002
0x0000000000000003
0x0000000000000004
*/
通过内存布局看到,数组内容需要跳过前面的32个字节。那么前面的字节分别存放着什么东西呢?
数组的容量会自动扩容至元素个数的两倍,且是8的倍数。
示例代码二:
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
*/
所以,数组的表象是结构体,但其本质是引用类型。