String
我们先来思考String变量占用多少内存?
var str1 = "0123456789"
print(Mems.size(ofVal: &str1)) // 16
print(Mems.memStr(ofVal: &str1)) // 0x3736353433323130 0xea00000000003938
我们通过打印可以看到String变量
占用了16个字节,并且打印内存布局,前后各占用了8个字节
下面我们再进行反汇编来观察下

可以看到这两句指令正是分配了前后8个字节给了String变量
那String变量底层存储的是什么呢?
我们通过上面看到String变量
的16个字节的值其实是对应转成的ASCII码值
ASCII码表的地址:https://www.ascii-code.com

我们看上图就可以得知,左边对应的是0~9的十六进制ASCII码值
,又因为小端模式
下高字节放高地址,低字节放低地址的原则,对比正是我们打印的16个字节中存储的数据
0x3736353433323130 0xea00000000003938
然后我们再看后8个字节前面的e
和a
分别代表的是类型
和长度
如果String
的数据是直接存储在变量中的,就是用e
来标明类型,如果要是存储在其他地方,就会用别的字母来表示
我们String
字符的长度正好是10,所以就是十六进制的a
var str1 = "0123456789ABCDE"
print(Mems.size(ofVal: &str1)) // 16
print(Mems.memStr(ofVal: &str1)) // 0x3736353433323130 0xef45444342413938
我们打印上面这个String变量
,发现表示长度的值正好变成了f
,而后7个字节也都被填满了,所以也证明了这种方式最多只能存储15个字节的数据
这种方式很像OC
中的Tagger Pointer
的存储方式
如果存储的数据超过15个字符,String变量又会是什么样呢?
我们改变String变量
的值,再进行打印观察
var str1 = "0123456789ABCDEF"
print(Mems.size(ofVal: &str1)) // 16
print(Mems.memStr(ofVal: &str1)) // 0xd000000000000010 0x80000001000079a0
我们发现String变量
的内存占用还是16个字节,但是内存布局已经完全不一样了
这时我们就需要借助反汇编来进一步分析了

看上图能发现最后还是会先后分配8个字节给String变量
,但不同的是在这之前会调用了函数,并将返回值给了String变量
的前8个字节
而且分别将字符串的值还有长度作为参数传递了进去,下面我们就看看调用的函数里具体做了什么


我们可以看到函数内部会将一个掩码的值和String变量
的地址值相加,然后存储到String变量
的后8个字节中
所以我们可以反向计算出所存储的数据真实地址值
0x80000001000079a0 - 0x7fffffffffffffe0 = 0x1000079C0
其实也就是一开始存储到rdi
中的值

通过打印真实地址值可以看到16个字节确实都是存储着对应的ASCII码值
那么真实数据是存储在什么地方呢?
通过观察它的地址我们可以大概推测是在数据段,为了更确切的认证我们的推测,使用MachOView
来直接查看在可执行文件中这句代码的真正存储位置
我们找到项目中的可执行文件,然后右键Show in Finder

然后右键通过MachOView
的方式来打开

最终我们发现在代码段中的字符串常量区中

对比两个字符串的存储位置
我们现在分别查看下这两个字符串的存储位置是否相同
var str1 = "0123456789"
var str2 = "0123456789ABCDEF"
我们还是用MachOView
来打开可执行文件,发现两个字符串的真实地址都是放在代码段中的字符串常量区,并且相差16个字节

然后我们再看打印的地址的前8个字节
0xd000000000000010 0x80000001000079a0
按照推测10
应该也是表示长度的十六进制,而前面的d
就代表着这种类型
我们更改下字符串的值,发现果然表示长度的值也随之变化了
var str2 = "0123456789ABCDEFGH"
print(Mems.size(ofVal: &str2)) // 16
print(Mems.memStr(ofVal: &str2)) // 0xd000000000000012 0x80000001000079a0
如果分别给两个String变量进行拼接会怎样呢?
var str1 = "0123456789"
str1.append("G")
print(Mems.size(ofVal: &str1)) // 16
print(Mems.memStr(ofVal: &str1)) // 0x3736353433323130 0xeb00000000473938
var str2 = "0123456789ABCDEF"
str2.append("G")
print(Mems.size(ofVal: &str2)) // 16
print(Mems.memStr(ofVal: &str2)) // 0xf000000000000011 0x0000000100776ed0
我们发现str1的后8个字节还有位置可以存放新的字符串,所以还是继续存储在内存变量里
而str2的内存布局不一样了,前8个字节可以看出来类型变成f
,字符串长度也变为十六进制的11
;而后8个字节的地址很像堆空间
的地址值
验证String变量的存储位置是否在堆空间
为了验证我们的推测,下面用反汇编来进行观察
我们在验证之前先创建一个类的实例变量,然后跟进去在内部调用malloc
的指令位置打上断点
class Person { }
var p = Person()

然后我们先将断点置灰,重新反汇编之前的Sting变量

然后将置灰的malloc
的断点点亮,然后进入

发现确实会进入到我们之前在调用malloc
的断点处,所以这就验证了确实会分配堆空间内存来存储String变量
的值了
我们还可以用LLDB
的指令bt
来打印调用栈详细信息来查看

发现也是在调用完append方法
之后就会进行malloc
的调用了,从这一层面也验证了我们的推测
那堆空间里存储的str2的值是怎样的呢?
然后我们过掉了append函数
后,打印str2的地址值,然后再打印后8个字节存放的堆空间地址值

其内部偏移了32个字节
后,正是我们String变量
的ASCII码值
总结
1.如果字符串长度小于等于0xF(十进制为15), 字符串内容直接存储到字符串变量的内存中,并以ASCII码值的小端模式来进行存储
第9个字节会存储字符串变量的类型和字符长度
var str1 = "0123456789"
print(Mems.size(ofVal: &str1)) // 16
print(Mems.memStr(ofVal: &str1)) // 0x3736353433323130 0xeb00000000473938
进行字符串拼接操作后
如果拼接后的字符串长度还是小于等于0xF(十进制为15),存储位置同未拼接之前
var str1 = "0123456789"
str1.append("ABCDE")
print(Mems.size(ofVal: &str1)) // 16
print(Mems.memStr(ofVal: &str1)) // 0x3736353433323130 0xef45444342413938
如果拼接后的字符串长度大于0xF(十进制为15),会开辟堆空间来存储字符串内容
字符串的地址值中,前8个字节存储字符串变量的类型和字符长度,后8个字节存储着堆空间的地址值,堆空间地址 + 0x20
可以得到真正的字符串内容
堆空间地址的前32个字节是用来存储描述信息的
由于常量区是程序运行之前就已经确定位置了的,所以拼接字符串是运行时操作,不可能再回存放到常量区,所以直接分配堆空间进行存储
var str1 = "0123456789"
str1.append("ABCDEF")
print(Mems.size(ofVal: &str1)) // 16
print(Mems.memStr(ofVal: &str1)) // 0xf000000000000010 0x000000010051d600
2.如果字符串长度大于0xF(十进制为15),字符串内容会存储在__TEXT.cstring
中(常量区)
字符串的地址值中,前8个字节存储字符串变量的类型和字符长度,后8个字节存储着一个地址值,地址值 & mask
可以得到字符串内容在常量区真正的地址值
var str2 = "0123456789ABCDEF"
print(Mems.size(ofVal: &str2)) // 16
print(Mems.memStr(ofVal: &str2)) // 0xd000000000000010 0x80000001000079a0
进行字符串拼接操作后,同上面开辟堆空间存储的方式
var str2 = "0123456789ABCDEF"
str2.append("G")
print(Mems.size(ofVal: &str2)) // 16
print(Mems.memStr(ofVal: &str2)) // 0xf000000000000011 0x0000000106232230
dyld_stub_binder
我们反汇编看到底层调用的String.init
方法其实是动态库里的方法,而动态库在内存中的位置是在Mach-O文件
的更高地址的位置,如下图所示

所以我们这里看到的地址值其实是一个假的地址值,只是用来占位的

我们再跟进发现其内部会跳转到另一个地址,取出其存储的真正需要调用的地址值去调用
下一个调用的地址值一般都是相差6个字节
0x10000774e + 0x6 = 0x100007754
0x100007754 + 0x48bc(%rip) = 0x10000C010
最后就是去0x10000C010地址中找到需要调用的地址值0x100007858


然后一直跟进,最后会进入到动态库的dyld_stub_binder
中进行绑定

最后才会真正进入到动态库中的String.init
执行指令,而且可以发现其真正的地址值非常大,这也能侧面证明动态库是在可执行文件更高地址的位置

然后我们在执行到下一个String.init
的调用

跟进去发现这是要跳转的地址值就已经是动态库中的String.init
真实地址值了


这也说明了dyld_stub_binder
只会执行一次,而且是用到的时候在进行调用,也就是延迟绑定
dyld_stub_binder
的主要作用就是在程序运行时,将真正需要调用的函数地址替换掉之前的占位地址
Array
我们来思考Array变量占用多少内存?
var array = [1, 2, 3, 4]
print(Mems.size(ofVal: &array)) // 8
print(Mems.ptr(ofVal: &array)) // 0x000000010000c1c8
print(Mems.ptr(ofRef: array)) // 0x0000000105862270
我们通过打印可以看到Array变量
占用了8个字节,其内存地址就是存储在全局区的地址
然而我们发现其内存地址的存储空间存储的地址值更像一个堆空间的地址
Array变量存储在什么地方呢?
带着疑问我们还是进行反汇编来观察下,并且在malloc
的调用指令处打上断点

发现确实调用了malloc
,那么就证明了Array变量
内部会分配堆空间

等执行完返回值给到Array变量
之后,我们打印Array变量
存储的地址值内存布局,发现其内部偏移32个字节
的位置存储着元素1、2、3、4
我们还可以直接通过打印内存结构来观察
var array = [1, 2, 3, 4]
print(Mems.memStr(ofRef: array))
//0x00007fff88a8dd18
//0x0000000200000003
//0x0000000000000004
//0x0000000000000008
//0x0000000000000001
//0x0000000000000002
//0x0000000000000003
//0x0000000000000004
我们调整一下元素数量,再打印观察
var array = [Int]()
for i in 1...8 {
array.append(i)
}
print(Mems.memStr(ofRef: array))
//0x00007fff88a8e460
//0x0000000200000003
//0x0000000000000008
//0x0000000000000010
//0x0000000000000001
//0x0000000000000002
//0x0000000000000003
//0x0000000000000004
//0x0000000000000005
//0x0000000000000006
//0x0000000000000007
//0x0000000000000008
发现第3段8个字节的位置也变成了8,等同我们添加的元素数量
而第4端8个字节的位置变成了16,说明扩大了一倍,可以推测这里存储的是容量的扩增
根据我们的反汇编和推测,Array变量
的内部结构如下图所示

Array、String
的底层结构都更像是引用类型的数据结构,只是表层作为值类型来使用
原文链接:http://www.cnblogs.com/funkyRay/p/swift-jin-jie-shi-yistringarray-de-di-ceng-fen-xi.html