探究Swift的String底层实现

语言: CN / TW / HK

问题: Swift中的字符串在内存中是如何存储的?

  • 首先我们来代码验证一下最简单的字符串--"空字符串"的内存分布 var empty = "" print("empty \(empty)") print(withUnsafePointer(to: &empty, { $0 }))

打印结果:

image.png

1. 查看Swift.String源码链接, 搜索"empty"

image.png

  • 看关键代码发现底部的public init方法调用了内部的init方法, 该初始化方法接收了一个_StringGuts的对象作为入参.
  • 源码357行开始, 也可看到结构体String持有_StringGuts作为成员变量. 我们继续深挖StringGuts.

2. 查看StringGuts.swift源码链接, 搜索"empty".

  • _StringGuts结构体的初始化方法中, 可以看到该结构体持有StringObject作为成员变量, 拨云见日, 深挖"StringObject". image.png

3. 查看StringObject源码链接, 搜索"empty".

  • StringObjectinit(count: variant: discirminator: flags:)方法中可以看到StringObject结构体的4个成员变量. image.png

  • 在创建一个字符串的过程中, 都存储了什么内容呢? image.png

  • 从上面的源码可以看到String结构体在底层存储的就是以上4个成员变量的内容.

  • 初始化方法中discriminator成员变量对应的的Nibbles又是什么呢? image.png

  • 可以看到Nibbles也是一个枚举类型, 但是这里只是定义, 原始定义是:

image.png - 可以看到, 这里调用的方法判断是如果当前是ASCII码, 那么当前的Discriminator判别器就是0xE000_0000_0000_0000, 如果不是ASCII码就是0xA000_0000_0000_0000 image.png

  • 查看一个空字符串的内存分布, 打印结果如下: image.png
  • 查看一个包含中文的字符串, 打印结果如下: image.png
  • ⭐️综上, 我们可以看出A, E在这里是用来标识当前是否是ASCII码, 其中后面的数字是用来代表当前的多少个字符串的长度.

  • _discriminator占据4位, 每一位的标识如下: image.png

image.png

  • 大字符串的规则和Nibbles的布局结构如下: image.png

image.png

  • 对于原生的Swift字符串来说, 采取的是tail-allocated(尾递归)存储, 也就是在当前实例分配有超出其最后存储属性的额外空间, 额外的空间可用于直接在实例中存储任意数据, 无需额外的堆分配.代码验证如下:

image.png - 接下来我们需要关注的是0x8000000100003f60这个值, 根据上面的源码分析阅读, 我们知道当前0x8标识的是大字符串, 这点我们在源码里也可以找到答案: image.png - 同时结合nibbles在内存中的布局我们知道其中b60:b0是存储字符串的地址, 当然这个地址要加上偏移量, 这个偏移量是32, 这里我们可通过计算器来验证一下:

image.png

下面是ASCII码十六进制符号对照表:

image.png

  • 那么前面的8个字节是什么呢? 我们可以先从初始化的流程来分析:

image.png

image.png

image.png - 所以可以看到, 除了我们当前的地址和标识位之外, 剩余的就是countAndFlags, 这里我们可以看到布局如下:

image.png

image.png - 第一个标志位是isASCII, 如果我们修改成中文, 这里就会改变

image.png

  • ⭐️综上, 我们可以发现Small strings(长度小于等于15的小字符串)直接存在内存中, Large strings(长度大于15的大字符串)存储的是内存地址

回答: Small strings(长度小于等于15的小字符串)直接存在内存中, Large strings(长度大于15的大字符串)存储的是内存地址

发文不易, 喜欢点赞的人更有好运气👍 :), 定期更新+关注不迷路~

ps:欢迎加入笔者18年建立的研究iOS审核及前沿技术的三千人扣群:662339934,坑位有限,备注“掘金网友”可被群管通过~