一. 汇编分析String底层
Mach-O文件是iOS的可执行文件,我们平时写的代码都在Mach-O,所以我们窥探Mach-O文件,就相当于窥探内存了(因为Mach-O文件载入内存不会有太大变化,只不过内存是动态更新的),如下图:
问题1
1个String变量占用多少内存?
下面2个String变量,底层存储有什么不同?
var str1 = "0123456789"
var str2 = "0123456789ABCDEF"
运行如下代码:
var str1 = "0123456789ABCDE"
print(MemoryLayout.stride(ofValue: str1)) //16 实际分配16字节
print(Mems.memStr(ofVal: &str1))
打印:
16
0x3736353433323130 0xef45444342413938
通过打印可知:
16:str1实际分配16字节,
0x3736353433323130 0xef45444342413938 :这是打印str1指针指向内存存储的东西,0x代表16进制,e代表直接把字符内容存储到内存中,f代表长度15,通过查询ASCII表可知0代表30,1代表31......等等
总结:
当字符串长度小于等于15,1个字节留着存放长度,另外15字节直接存放字符串的ASCII值,类似于OC的tagger pointer技术
如果字符串再多一位呢?
var str2 = "0123456789ABCDEF"
print(MemoryLayout.stride(ofValue: str2)) //16 实际分配16
print(Mems.memStr(ofVal: &str2))
打印:
16
0xd000000000000010 0x800000010000a790
可以发现,当字符串长度大于15,就不是直接存储字符串的值了
MJ老师通过窥探汇编得出如下结果,对于0xd000000000000010 0x800000010000a790:
- 前8字节存放的是字符创的长度10,在16进制中就是16
- 后8个字节存放的是:0x800000010000a790 = 字符串的真实地址 + 0x7fffffffffffffe0,所以字符串的真实地址 = 0x800000010000a790 - 0x7fffffffffffffe0(小技巧:字符串的真实地址 = 0x000000010000a790 + 0x20)
通过计算得出0x10000A7B0是"0123456789ABCDEF"的真实地址,读取这个地址的内存,如下:
x 0x10000a7b0: 30 31 32 33 34 35 36 37 38 39 41 42 43 44 45 46 0123456789ABCDEF
可以发现,这个内存地址存储的的确是str2字符串。
- 使用MachoView
那个这个地址:0x10000A7B0在哪呢?
这就要用到文章开头的知识了,我们窥探Mach-O文件,就相当于窥探内存,在Mach-O文件中我们可以知道0x10000A7B0地址存放在哪里
补充:
0x100000000: VM Address 虚拟地址
0x10000A7B0 = 0x100000000 + 0xA7B0
0xA7B0是Mach文件的地址,所以我们在Mach-O文件中找0xA7800就好了
运行程序,使用MachoView打开程序的可执行文件,可以发现0xA7B0放在常量区,如下:
总结:
字符串长度 <= 0xF(15),字符串内容直接存放在str1变量的内存中(比如:var str1 = "0123456789")
字符串长度 > 0xF(15),字符串内容存放在__TEXT.cstring中(常量区)
字符串的地址值信息存放在str2变量的后8个字节中(比如:var str2 = "0123456789ABCDEFGHIJ")
问题2
如果对String进行拼接操作,String变量的存储会发生什么变化?
var str1 = "0123456789"
var str2 = "0123456789ABCDEF"
str1.append("ABCDE")
str1.append("F")
str2.append("G")
运行如下代码:
var str1 = "01234567"
print(Mems.memStr(ofVal: &str1))
str1.append("GIHJ")
print(Mems.memStr(ofVal: &str1))
打印:
0x3736353433323130 0xe800000000000000
0x3736353433323130 0xec0000004a484947
可以发现,当字符串进行拼接的时候,如果拼接后字符串长度还是小于等于15,那么拼接的字符串还会放在原来的后面
如果字符串长度本来是16,拼接后大于16呢?
var str2 = "0123456789ABCDEF"
print(Mems.memStr(ofVal: &str2))
str2.append("G")
print(Mems.memStr(ofVal: &str2))
打印:
0xd000000000000010 0x8000000100006620 //16字节,放常量区
0xf000000000000011 0x000000010068af60 //大于16字节,放堆空间
根据上面的小技巧,0x000000010068af60 + 0x20得到字符串的真实地址:0x000000010068af80,查看真实地址内存,发现存储的的确是上面的字符串
(lldb) x 0x000000010068af80
0x10068af80: 30 31 32 33 34 35 36 37 38 39 41 42 43 44 45 46 0123456789ABCDEF
0x10068af90: 47 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 G...............
(lldb)
如何证明上面str2.append("G")之后是存放在堆空间?
很简单,在malloc函数打个断点,如果走malloc函数,就说明在堆空间
MJ老师在汇编里面在malloc函数打个断点,执行str2.append("G")发现的确走了malloc,说明str2.append("G")之后是存放在堆空间
大总结:
现在可以回答开头两个问题了
var str1 = "0123456789"
字符串长度 <= 0xF(15),字符串内容直接存放在str1变量的内存中
var str2 = "0123456789ABCDEF"
字符串长度 > 0xF(15),字符串内容存放在__TEXT.cstring中(常量区)
字符串的地址值信息存放在str2变量的后8个字节中
str1.append("ABCDE")
由于字符串长度 <= 0xF,所以字符串内容依然存放在str1变量的内存中
str1.append("F")
开辟堆空间
可能你会疑问这里为什么是开辟堆空间?
拼接之前str1是0123456789ABCDE,这时候是字符串15字节+1字节(存放长度),16个字节已经满了,所以无法拼接。
那么放常量区呢?更不可以,因为常量区的内容不可以改,所以只能开辟堆空间。
str2.append("G")
开辟堆空间
二. 汇编分析Array底层
关于Array的思考
public struct Array
var arr = [1, 2, 3, 4]
1个Array变量占用多少内存?
数组中的数据存放在哪里?