Swift进阶-String源码解析

Swift进阶-类与结构体
Swift-函数派发
Swift进阶-属性
Swift进阶-指针
Swift进阶-内存管理
Swift进阶-TargetClassMetadata和TargetStructMetadata数据结构源码分析
Swift进阶-Mirror解析
Swift进阶-闭包
Swift进阶-协议
Swift进阶-泛型
Swift进阶-String源码解析
Swift进阶-Array源码解析

1.String在内存中是如何存储的

创建一个空的字符串发生了什么?

var empty = "" 
print(empty) // 断点在这里调试看
断点调试x/8g

这里并不能看出String的内存结构。那么接下来就借助Swift源码的方式看看String在内存中到底是如何存储的。

打开swift源码 -> stdib里的String.swift

@frozen
public struct String {
  public // @SPI(Foundation)
  var _guts: _StringGuts

  @inlinable @inline(__always)
  internal init(_ _guts: _StringGuts) {
    self._guts = _guts
    _invariantCheck()
  }

  // This is intentionally a static function and not an initializer, because
  // an initializer would conflict with the Int-parsing initializer, when used
  // as function name, e.g.
  //   [1, 2, 3].map(String.init)
  @_alwaysEmitIntoClient
  @_semantics("string.init_empty_with_capacity")
  @_semantics("inline_late")
  @inlinable
  internal static func _createEmpty(withInitialCapacity: Int) -> String {
    return String(_StringGuts(_initialCapacity: withInitialCapacity))
  }

  /// Creates an empty string.
  ///
  /// Using this initializer is equivalent to initializing a string with an
  /// empty string literal.
  ///
  ///     let empty = ""
  ///     let alsoEmpty = String()
  @inlinable @inline(__always)
  @_semantics("string.init_empty")
  public init() { self.init(_StringGuts()) }
}

最直观地可以看到String是一个结构体,就是我们所说的值类型;它有一个成员变量_StringGuts

其中最后有一个创建空字符串初始化方式 self.init(_StringGuts())

空字符串初始化

接下来看看这个_StringGuts到底是什么东西?
同样找到swift源码 -> stdib里的StringGuts.swift

@frozen
public // SPI(corelibs-foundation)
struct _StringGuts: @unchecked Sendable {
  @usableFromInline
  internal var _object: _StringObject

  @inlinable @inline(__always)
  internal init(_ object: _StringObject) {
    self._object = object
    _invariantCheck()
  }

  // Empty string
  @inlinable @inline(__always)
  init() {
    self.init(_StringObject(empty: ()))
  }
}

_StringGuts也是一个结构体,它有一个成员变量是_StringObject类型的实例;
并且在最后是通过初始化出一个_StringObject类型的实例来初始化_StringGuts的。

所以真正swift的String的实质就是_StringObject。接下来看看_StringObject到底是什么玩意儿?

找到swift源码 -> stdib里的StringObject.swift,可以看到_StringObject是一个结构体,再找到空字符串的初始化函数:

_StringObject空字符串初始化函数

ps: 注意这里初始化时的传参,下面会说到这几个成员

最终找到字符串最终初始化函数,该函数是对成员的初始化赋值,那么只要搞懂这几个成员是代表什么意思,那就能搞清楚字符串的底层实质了。

_StringObject最终初始化函数

_StringObject存储着一些成员变量,文章最开始使用x/8g格式化输出一个空字符串对象empty的时候,那我猜测:输出的内容应该就是_StringObject里的_count、_variant、_discriminator、_flags。

  @usableFromInline
  internal var _count: Int // 字符串大小

  @usableFromInline
  internal var _variant: Variant // 枚举值  默认0

  @usableFromInline
  internal var _discriminator: UInt8 // 在初始化的时候传递了一个Nibbles.emptyString

  @usableFromInline
  internal var _flags: UInt16

internal var _variant: Variant是一个枚举值,默认是immortal 0:

@usableFromInline @frozen
  internal enum Variant {
    case immortal(UInt) // 原始字符串
    case native(AnyObject) // AnyObject
    case bridged(_CocoaString) // NSString

    @inlinable @inline(__always)
    internal static func immortal(start: UnsafePointer<UInt8>) -> Variant {
      let biased = UInt(bitPattern: start) &- _StringObject.nativeBias
      return .immortal(biased)
    }

    @inlinable @inline(__always)
    internal var isImmortal: Bool {
      if case .immortal = self { return true }
      return false
    }
  }

internal var _discriminator: UInt8 在初始化的时候传递了一个Nibbles.emptyString(Nibbles是一个枚举类型):

extension _StringObject.Nibbles {
  // The canonical empty string is an empty small string
  @inlinable @inline(__always)
  internal static var emptyString: UInt64 {
    return _StringObject.Nibbles.small(isASCII: true)
  }
}
// Discriminator for small strings
  @inlinable @inline(__always)
  internal static func small(isASCII: Bool) -> UInt64 {
    // 是ASCII就用0xE000_0000_0000_0000否则用0xA000_0000_0000_0000
    return isASCII ? 0xE000_0000_0000_0000 : 0xA000_0000_0000_0000
  }

0xE000_0000_0000_0000 与文章最上面截图相对应起来了:

x/8g空字符串对象地址

那接下来我们就能测试一下字符串了:

字符a的ASCII编码是97,97的16进制是61,注意那个2的字节位的输出

小于等于15个字符串时,会记录字符串的位数。
对于小字符串(小于等于15个字符串)来说,是优先直接存到内存当中,无需另外分配内存空间的。(和NSString差不多类似)

接下来看看中文字符

中文字符串

中文字符不是ASCII编码,一个中文字符占据3个字节(24位),也是我们上面通过源码分析得出的使用了 0xA000_0000_0000_0000

所以_StringObject.Nibbles是一个识别器,去识别字符串是不是ASCII编码。

对于大字符串(大于15个字符串)来说,原本的小字符串占据的15个字节已经不足以存储字符串了,那就会发生改变:

来看看0x8000000000000000在源码中出现的定义是一个大原始字符串:

那剩下的 0x000000010000b860 到底是什么东西呢?它是字符串的内存相对地址;
那应该偏移多少呢?来看源码里的注解

image.png

意思是0x10000b860需要加上偏移量nativeBias即32,32的16进制是0x20:
0x10000b860 + 0x20 = 0x10000b880

在源码注解里找到大字符串标志位

大字符串标志位

大字符串前8位就记录着这些标志位信息,0xd000000000000012就是大字符串前8位,拿到科学计算器里看看标志位:

所以count是0x12,转换成10进制就是18,正好对应18个字符。

总结:
小字符串:大小在15字节以内(包含15),直接存到内存当中,无需另外分配内存空间的(和NSString差不多类似)。
大字符串:大小在15字节以上,会另外开辟内存去存储字符串内容。字符串对象的第一个8字节存储的是标志位信息;第二个最高位标识大字符串,剩下的是相对偏移地址信息,需要偏移0x20才是真实另外开辟的内存地址。

二、String.Index

对于String来说,它并不支持通过下标的方式获取字符

只能通过String.Index的方式来访问

var str = "hello world"
// str.index(str.startIndex, offsetBy: 1) 从开始位置向后偏移1,返回结果是String.Index类型
print(str[str.index(str.startIndex, offsetBy: 1)]) // e

对于 Swift 来说,String 是一系列字符的集合,也就意味着 String 中的每一个元素是不等长的。那也就意味着我们在进行内存移动的时候步长是不一样的,什么意思?
比如我们有一个 Array 的数组(Int 类型),当我们遍历数组中的元素的时候,因为每个元素的内存大小是一致的,所以每次的偏移量就是 8 个字节。

但是对于字符串来说不一样,比如我要方位 str[1] 那么我是不是要把我这个字段遍历完成之后才能够确定是的偏移量?
依次内推每一次都要重新遍历计算偏移量,这个时候无疑增加了很多的内存消耗。这就是为什么我们不能通过 Int 作为下标来去访问 String

可以很直观的看到 Index 的定义:

String.Index64位的位域信息的内容

position aka encodedOffset一个 48 bit 值,用来记录码位偏移量;
transcoded offset: 一个 2 bit 的值,用来记录字符使用的码位数量;
grapheme cache: 一个 6 bit 的值,用来记录下一个字符的边界;
reserved : 7 bit 的预留字段;
scalar aligned : 一个 1 bit 的值,用来记录标量是否已经对齐过。

String.Index的本质就是一个64位的位域信息,这个位域信息展示的就是上面的解释。

创建String.Index实际上就是通过encodedOffset 或者 transcoded offsetencodedOffset就是方便我们从内存中通过下标访问到字符串。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容