Swift 进阶(十一)String、Array的底层分析

String

我们先来思考String变量占用多少内存?

var str1 = "0123456789"
print(Mems.size(ofVal: &str1)) // 16
print(Mems.memStr(ofVal: &str1)) // 0x3736353433323130 0xea00000000003938

我们通过打印可以看到String变量占用了16个字节,并且打印内存布局,前后各占用了8个字节

下面我们再进行反汇编来观察下

-w715

可以看到这两句指令正是分配了前后8个字节给了String变量

那String变量底层存储的是什么呢?

我们通过上面看到String变量的16个字节的值其实是对应转成的ASCII码值

ASCII码表的地址:https://www.ascii-code.com

-w1139

我们看上图就可以得知,左边对应的是0~9的十六进制ASCII码值,又因为小端模式下高字节放高地址,低字节放低地址的原则,对比正是我们打印的16个字节中存储的数据

0x3736353433323130 0xea00000000003938

然后我们再看后8个字节前面的ea分别代表的是类型长度

如果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个字节,但是内存布局已经完全不一样了

这时我们就需要借助反汇编来进一步分析了

-w998

看上图能发现最后还是会先后分配8个字节给String变量,但不同的是在这之前会调用了函数,并将返回值给了String变量的前8个字节

而且分别将字符串的值还有长度作为参数传递了进去,下面我们就看看调用的函数里具体做了什么

-w995
-w1058

我们可以看到函数内部会将一个掩码的值和String变量的地址值相加,然后存储到String变量的后8个字节中

所以我们可以反向计算出所存储的数据真实地址值

0x80000001000079a0 - 0x7fffffffffffffe0 = 0x1000079C0

其实也就是一开始存储到rdi中的值

-w640

通过打印真实地址值可以看到16个字节确实都是存储着对应的ASCII码值

那么真实数据是存储在什么地方呢?

通过观察它的地址我们可以大概推测是在数据段,为了更确切的认证我们的推测,使用MachOView来直接查看在可执行文件中这句代码的真正存储位置

我们找到项目中的可执行文件,然后右键Show in Finder

-w357

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

-w498

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

-w1055

对比两个字符串的存储位置

我们现在分别查看下这两个字符串的存储位置是否相同

var str1 = "0123456789"
var str2 = "0123456789ABCDEF"

我们还是用MachOView来打开可执行文件,发现两个字符串的真实地址都是放在代码段中的字符串常量区,并且相差16个字节

-w1165

然后我们再看打印的地址的前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()

-w979

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

-w979

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

-w978

发现确实会进入到我们之前在调用malloc的断点处,所以这就验证了确实会分配堆空间内存来存储String变量的值了

我们还可以用LLDB的指令bt来打印调用栈详细信息来查看

-w979

发现也是在调用完append方法之后就会进行malloc的调用了,从这一层面也验证了我们的推测

那堆空间里存储的str2的值是怎样的呢?

然后我们过掉了append函数后,打印str2的地址值,然后再打印后8个字节存放的堆空间地址值

-w981

其内部偏移了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文件的更高地址的位置,如下图所示

-w939

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

-w999

我们再跟进发现其内部会跳转到另一个地址,取出其存储的真正需要调用的地址值去调用

下一个调用的地址值一般都是相差6个字节

0x10000774e + 0x6 = 0x100007754
0x100007754 + 0x48bc(%rip) = 0x10000C010
最后就是去0x10000C010地址中找到需要调用的地址值0x100007858

-w998
-w997

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

-w996

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

-w1000

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

-w997

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

-w997
-w999

这也说明了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的调用指令处打上断点

-w988

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

-w1000

等执行完返回值给到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变量的内部结构如下图所示

-w503

Array、String的底层结构都更像是引用类型的数据结构,只是表层作为值类型来使用

posted on 2021-03-31 02:33  FunkyRay  阅读(263)  评论(0编辑  收藏  举报