一:String 的內(nèi)存布局
1. String 源碼分析
在 Swift源碼 中找到 String.swift 文件并定位到 String 的定義。
@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()) }
}
通過源碼可以發(fā)現(xiàn) String 的本質(zhì)是一個結(jié)構(gòu)體,并且有一個 _StringGuts 類型的成員變量 _guts 。在初始化的時候需要傳入 _StringGuts 類型的參數(shù)。接著找到 StringGuts.swift 文件,并定位到 _StringGuts 的定義。
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: ()))
}
}
可以發(fā)現(xiàn) _StringGuts 也是一個結(jié)構(gòu)體,并且遵守了協(xié)議 Sendable , 有一個 _StringObject 類型的成員變量 _object,并且初始化的時候需要傳入 _StringObject 類型的參數(shù)。接著找到 StringObject.swift 文件,并定位到 _StringObject 的定義。
@frozen @usableFromInline
internal struct _StringObject {
// Namespace to hold magic numbers
@usableFromInline @frozen
enum Nibbles {}
// Abstract the count and performance-flags containing word
@frozen @usableFromInline
struct CountAndFlags {
@usableFromInline
var _storage: UInt64
@inlinable @inline(__always)
internal init(zero: ()) { self._storage = 0 }
}
#if arch(i386) || arch(arm) || arch(arm64_32) || arch(wasm32)
@usableFromInline @frozen
internal enum Variant {
case immortal(UInt)
case native(AnyObject)
case bridged(_CocoaString)
@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
}
}
@usableFromInline
internal var _count: Int
@usableFromInline
internal var _variant: Variant
@usableFromInline
internal var _discriminator: UInt8
@usableFromInline
internal var _flags: UInt16
@inlinable @inline(__always)
init(count: Int, variant: Variant, discriminator: UInt64, flags: UInt16) {
_internalInvariant(discriminator & 0xFF00_0000_0000_0000 == discriminator,
"only the top byte can carry the discriminator and small count")
self._count = count
self._variant = variant
self._discriminator = UInt8(truncatingIfNeeded: discriminator &>> 56)
self._flags = flags
self._invariantCheck()
}
@inlinable @inline(__always)
init(variant: Variant, discriminator: UInt64, countAndFlags: CountAndFlags) {
self.init(
count: countAndFlags.count,
variant: variant,
discriminator: discriminator,
flags: countAndFlags.flags)
}
@inlinable @inline(__always)
internal var _countAndFlagsBits: UInt64 {
let rawBits = UInt64(truncatingIfNeeded: _flags) &<< 48
| UInt64(truncatingIfNeeded: _count)
return rawBits
}
#else
//
// Laid out as (_countAndFlags, _object), which allows small string contents
// to naturally start on vector-alignment.
//
@usableFromInline
internal var _countAndFlagsBits: UInt64
@usableFromInline
internal var _object: Builtin.BridgeObject
@inlinable @inline(__always)
internal init(zero: ()) {
self._countAndFlagsBits = 0
self._object = Builtin.valueToBridgeObject(UInt64(0)._value)
}
#endif
@inlinable @inline(__always)
internal var _countAndFlags: CountAndFlags {
_internalInvariant(!isSmall)
return CountAndFlags(rawUnchecked: _countAndFlagsBits)
}
}
我們可以看到 _StringObject 也是一個結(jié)構(gòu)體,并且有 4 個成員變量分別是
-
_count:Int類型 -
_variant:variant類型 -
_discriminator:UInt8類型 -
_flags:UInt16類型
在_StringGuts的定義中能夠看見空字符串創(chuàng)建時調(diào)用了_StringObject的empty:()函數(shù),那么找到這個函數(shù)的定義
extension _StringObject {
@inlinable @inline(__always)
internal init(_ small: _SmallString) {
// Small strings are encoded as _StringObjects in reverse byte order
// on big-endian platforms. This is to match the discriminator to the
// spare bits (the most significant nibble) in a pointer.
let word1 = small.rawBits.0.littleEndian
let word2 = small.rawBits.1.littleEndian
#if arch(i386) || arch(arm) || arch(arm64_32) || arch(wasm32)
// On 32-bit, we need to unpack the small string.
let smallStringDiscriminatorAndCount: UInt64 = 0xFF00_0000_0000_0000
let leadingFour = Int(truncatingIfNeeded: word1)
let nextFour = UInt(truncatingIfNeeded: word1 &>> 32)
let smallDiscriminatorAndCount = word2 & smallStringDiscriminatorAndCount
let trailingTwo = UInt16(truncatingIfNeeded: word2)
self.init(
count: leadingFour,
variant: .immortal(nextFour),
discriminator: smallDiscriminatorAndCount,
flags: trailingTwo)
#else
// On 64-bit, we copy the raw bits (to host byte order).
self.init(rawValue: (word1, word2))
#endif
_internalInvariant(isSmall)
}
@inlinable
internal static func getSmallCount(fromRaw x: UInt64) -> Int {
return Int(truncatingIfNeeded: (x & 0x0F00_0000_0000_0000) &>> 56)
}
@inlinable @inline(__always)
internal var smallCount: Int {
_internalInvariant(isSmall)
return _StringObject.getSmallCount(fromRaw: discriminatedObjectRawBits)
}
@inlinable
internal static func getSmallIsASCII(fromRaw x: UInt64) -> Bool {
return x & 0x4000_0000_0000_0000 != 0
}
@inlinable @inline(__always)
internal var smallIsASCII: Bool {
_internalInvariant(isSmall)
return _StringObject.getSmallIsASCII(fromRaw: discriminatedObjectRawBits)
}
@inlinable @inline(__always)
internal init(empty:()) {
// Canonical empty pattern: small zero-length string
#if arch(i386) || arch(arm) || arch(arm64_32) || arch(wasm32)
self.init(
count: 0,
variant: .immortal(0),
discriminator: Nibbles.emptyString,
flags: 0)
#else
self._countAndFlagsBits = 0
self._object = Builtin.valueToBridgeObject(Nibbles.emptyString._value)
#endif
_internalInvariant(self.smallCount == 0)
_invariantCheck()
}
}
在 _StringObject 的擴展中可以找到 empty:() 函數(shù)的定義,能夠看到這里區(qū)分了架構(gòu),對于我們來說只需要看第一個分支就可以了,發(fā)現(xiàn)這里調(diào)用了一個方法 count: variant: discriminator: flags:, 找到這個方法的定義
@inlinable @inline(__always)
init(count: Int, variant: Variant, discriminator: UInt64, flags: UInt16) {
_internalInvariant(discriminator & 0xFF00_0000_0000_0000 == discriminator,
"only the top byte can carry the discriminator and small count")
self._count = count
self._variant = variant
self._discriminator = UInt8(truncatingIfNeeded: discriminator &>> 56)
self._flags = flags
self._invariantCheck()
}
可以看見這里就是在對 _StringObject 的成員變量進行賦值操作。那么這幾個成員變量分別代表著什么意思呢?
1.1 _count
_count 是 Int 類型, 從字面意思其實也不難理解就是字符串大小的意思
1.2 _variant
_variant 是 Variant 類型 找到 Variant 的定義
internal enum Variant {
case immortal(UInt)
case native(AnyObject)
case bridged(_CocoaString)
可以看見 Variant 是一個枚舉類型,代表著字符串的三種情況,分別為 immortal、native 以及 bridged 。而通過剛才初始化方法的傳值,此時的 _variant 類型是 .immortal(0) 類型的,這個代表 Swift 原生的字符串類型。native 著代表著 AnyObject 。bridged 代表著 _CocoaString 也就是 NSString 。
1.3 _discriminator
_discriminator 是 UInt8 類型,在初始化方法中我們發(fā)現(xiàn)傳入了 Nibbles.emptyString 值,定位到 Nibbles 的定義
enum Nibbles {}
發(fā)現(xiàn)這是一個什么 case 都沒有的枚舉,但是在 StringObject.swift 文件的下面還有一些 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)
}
}
extension _StringObject.Nibbles {
// Mask for address bits, i.e. non-discriminator and non-extra high bits
@inlinable @inline(__always)
static internal var largeAddressMask: UInt64 { return 0x0FFF_FFFF_FFFF_FFFF }
// Mask for address bits, i.e. non-discriminator and non-extra high bits
@inlinable @inline(__always)
static internal var discriminatorMask: UInt64 { return ~largeAddressMask }
}
extension _StringObject.Nibbles {
// Discriminator for small strings
@inlinable @inline(__always)
internal static func small(isASCII: Bool) -> UInt64 {
return isASCII ? 0xE000_0000_0000_0000 : 0xA000_0000_0000_0000
}
// Discriminator for small strings
@inlinable @inline(__always)
internal static func small(withCount count: Int, isASCII: Bool) -> UInt64 {
_internalInvariant(count <= _SmallString.capacity)
return small(isASCII: isASCII) | UInt64(truncatingIfNeeded: count) &<< 56
}
// Discriminator for large, immortal, swift-native strings
@inlinable @inline(__always)
internal static func largeImmortal() -> UInt64 {
return 0x8000_0000_0000_0000
}
// Discriminator for large, mortal (i.e. managed), swift-native strings
@inlinable @inline(__always)
internal static func largeMortal() -> UInt64 { return 0x0000_0000_0000_0000 }
internal static func largeCocoa(providesFastUTF8: Bool) -> UInt64 {
return providesFastUTF8 ? 0x4000_0000_0000_0000 : 0x5000_0000_0000_0000
}
}
可以在第一個擴展中找到 emptyString 返回的是 Nibbles 的 small(isASCII:) 方法,true 代表是 ASCII,false 代表不是。在第三個擴展中我們看到 small(isASCII: Bool) 方法的實現(xiàn),當是 ASCII 碼的時候返回的是 0xE000_0000_0000_0000,當不是 ASCII 碼的時候返回的是 0xA000_0000_0000_0000
1.4 小字符串
對于空字符串通過上面的源碼能夠得到 _discriminator 的值應(yīng)該是 0xE000_0000_0000_0000 接下來驗證一下

此時看到當前的字符串打印出來屬于 0xE000_0000_0000_0000,這個空字符串屬于 ASCII,這與源碼一致。
我們知道中文字符不是 ASCII 碼,那么將字符串賦值中文,那么這里是否會打印 0xA000_0000_0000_0000 呢?我們來試一下。

能夠發(fā)現(xiàn)這里打印的就是 0xA000_0000_0000_0000 這與我們猜想的一致。那 0xa 后面的 6 是什么意思? 前面的 0x0000bda5e5a882e6 又是什么?
這里為了方法觀察,將字符串賦值成英文字符。

將字符串賦值成 aa 后,能夠發(fā)現(xiàn)第一個 8 字節(jié)存儲的是 0x0000000000006161,而我們知道 a 的 ASCII 的值是 97,而 97 的 16 進制就是 61 ,而 0xe 后面的值是 2,是不是就代表著 _count ?接著試一試 abc

字符串是
abc 的時候,第一個 8 字節(jié)存儲的正是 abc 所對應(yīng)的 ASCII 碼的 16 進制。并且 0xe 后面這時已經(jīng)變成了 3 。我們知道字符串的大小是 16 字節(jié),那么對于小字符串 (長度小于 16 ) 它的值是否就直接存儲在這 16 字節(jié)中?

我們可以看到字符串的長度為 15 時 剛好占滿這 16 字節(jié)。
1.5 大字符串
那對于長度超過 15 的大字符串,這又是怎么存儲的呢?

我們可以看到當字符串的長度大于 15 的時候這里的存儲內(nèi)容就發(fā)生了變化,第二個 8 字節(jié)變成了 0x8 開頭,這是找到源碼中的 0x8000000000000000
extension _StringObject.Nibbles {
// Discriminator for small strings
@inlinable @inline(__always)
internal static func small(isASCII: Bool) -> UInt64 {
return isASCII ? 0xE000_0000_0000_0000 : 0xA000_0000_0000_0000
}
// Discriminator for small strings
@inlinable @inline(__always)
internal static func small(withCount count: Int, isASCII: Bool) -> UInt64 {
_internalInvariant(count <= _SmallString.capacity)
return small(isASCII: isASCII) | UInt64(truncatingIfNeeded: count) &<< 56
}
// Discriminator for large, immortal, swift-native strings
@inlinable @inline(__always)
internal static func largeImmortal() -> UInt64 {
return 0x8000_0000_0000_0000
}
// Discriminator for large, mortal (i.e. managed), swift-native strings
@inlinable @inline(__always)
internal static func largeMortal() -> UInt64 { return 0x0000_0000_0000_0000 }
internal static func largeCocoa(providesFastUTF8: Bool) -> UInt64 {
return providesFastUTF8 ? 0x4000_0000_0000_0000 : 0x5000_0000_0000_0000
}
}
可以看到這里的 largeImmortal() 方法返回的是 0x8000_0000_0000_0000 代表著原生字符串的大字符串
extension _StringObject {
@inlinable @inline(__always)
internal init(immortal bufPtr: UnsafeBufferPointer<UInt8>, isASCII: Bool) {
let countAndFlags = CountAndFlags(
immortalCount: bufPtr.count, isASCII: isASCII)
#if arch(i386) || arch(arm) || arch(arm64_32) || arch(wasm32)
self.init(
variant: .immortal(start: bufPtr.baseAddress._unsafelyUnwrappedUnchecked),
discriminator: Nibbles.largeImmortal(),
countAndFlags: countAndFlags)
#else
// We bias to align code paths for mortal and immortal strings
let biasedAddress = UInt(
bitPattern: bufPtr.baseAddress._unsafelyUnwrappedUnchecked
) &- _StringObject.nativeBias
self.init(
pointerBits: UInt64(truncatingIfNeeded: biasedAddress),
discriminator: Nibbles.largeImmortal(),
countAndFlags: countAndFlags)
#endif
}
...
}
largeImmortal() 方法在初始化的時候調(diào)用,并且將 largeImmortal() 的返回值賦值給了_discriminator , 這與我們之前研究小字符串時的邏輯是一致的。
那么0xd000000000000011 和 0x0000000100003f60 這兩個地址又是什么呢?
1.5.1 前 8 字節(jié)
0xd000000000000011 這個存儲的到底是什么呢?
在上面的初始化方法中我們看到調(diào)用的是 self.init(variant: discriminator: countAndFlags:) 這個初始化方法,這個傳了一個參數(shù)叫做 countAndFlags 并且 countAndFlags 等于 CountAndFlags( immortalCount: bufPtr.count, isASCII: isASCII) 定位到 CountAndFlags 的初始化方法 immortalCount: isASCII:
extension _StringObject.CountAndFlags {
...
//
// Specialized initializers
//
@inlinable @inline(__always)
internal init(immortalCount: Int, isASCII: Bool) {
self.init(
count: immortalCount,
isASCII: isASCII,
isNFC: isASCII,
isNativelyStored: false,
isTailAllocated: true)
}
...
在這里找到了這個方法的定義。我們注意到這個 _StringObject.CountAndFlags 擴展的上方有著蘋果官方留下的注釋

通過閱讀這個注釋,我們了解到
-
isASCII:用來判斷當前字符串是否是ASCII,在高63位。 -
isNFC:這個默認為1,在高62位。 -
isNativelyStored:是否是原生存儲,在 高61位。 -
isTailAllocated:是否是尾部分配的,在 高60位。 -
TBD:留作將來使用,在高59位到高48位。 -
count:當前字符串的大小,在高47位到低0位。
利用蘋果電腦的計算器(程序員型) 來看一下0xd000000000000011
0xd000000000000011.png
高4位1101與代碼和注釋一致,那么16進制的0x11的10進制 就是17而我們的字符串是abcdefghijklmnopq正好17個 所以大字符串的前8位地址存儲的是countAndFlags
1.5.2 后 8 字節(jié)
0x8000000100003f60 對與后 8 字節(jié)之前已經(jīng)知道了 0x8000_0000_0000_0000 代表著大原生字符串。那么后面的 0x100003f60 代表著什么呢?通過源碼找到了蘋果留下的另一段注釋

通過官方的注釋得知大字符串可以是原生的、共享的或者是外來的。我們這里主要探究原生的字符串。根據(jù)官方的注釋,這個原生的字符串具有尾部分配 ( tail-allocated ) 的存儲空間,它從存儲對象地址的 nativeBias 偏移量開始。這個偏移量是 32。通過前 8 字節(jié)的分析我們得到 isTailAllocated 是 1 ,這里也就和前面我們分析的相呼應(yīng)。
接下來看一下 discriminator(鑒別器) 和 objectAddr 的地址分配方式,根據(jù)官方給的注釋,這個 discriminator 在 后 8 字節(jié)中,占據(jù)的位置是高 63 位到高 60 位。高 60 位到低 0 位存儲的就是這個額外的存儲空間的內(nèi)存地址。
這個 objectAddr 存儲的是這個額外的存儲空間的內(nèi)存地址,但是它是一個相對地址,因為它需要加上 nativeBias,得到的才是這個額外的存儲空間的地址值。
那也就是意味著當字符串是大字符串的時候,會分配額外的存儲空間,用這個額外的存儲空間存儲字符串的值。
那么對于 0x100003f60 這個地址來說就是 objectAddr , 這個地址再偏移 32 位得到的地址就是存儲字符串的值的地址。
0x100003f60 + 32 = 0x100003f60 + 0x20 = 0x100003f80

1.6 String的內(nèi)存結(jié)構(gòu)總結(jié)
- 一個
String變量/常量的大小為16個字節(jié)。 - 當字符串的大小小于等于
15的時候為小字符串,當字符串的大小大于15的時候為大字符串。 - 小字符串時,前
15個字節(jié)用來存儲字符串的值,最后一個字節(jié)記錄當前字符串是否是ASCII和字符串的大小count。 - 大字符串時,前
8個字節(jié)用來記錄字符串的大小和其它的一些信息countAndFlags,比如是否是ASCII。后8個字節(jié)中,高63位到高60位存儲的是鑒別器(discriminator)的值,剩余的用來存儲相對偏移地址(objectAddr),這個地址需要再偏移32位才是存儲字符串的值的地址。
二. String.index
在 Swift 中對于 String 我們想要訪問到某一個字符可以通過 Index 去獲取
let string = "大家好"
// 從開始位置向后偏移1,返回結(jié)果是String.Index類型
let index = string.index(string.startIndex, offsetBy: 1);
print(string[index]);
這里能夠通過 string[index] 這樣的方式去獲取一個字符,那么為什么不能像數(shù)組那樣直接傳入一個數(shù)字而且必須要傳入一個 Swift.Index 類型呢?

2.1 Swift 中 String 的本質(zhì)
Swift 中的 String 代表的是一系列的 characters(字符),字符的表示方式有很多種,比如我們最熟悉的 ASCII 碼,ASCII 碼一共規(guī)定了 128 個字符的編碼,對于英文字符來說 128 個字符已經(jīng)夠用了,但是相對于其他語言來說,這是遠遠不夠用的,比如中國漢字。這也就意味著不同國家不同語言都需要有自己的編碼格式,這個時候同一個二進制文件就有可能被翻譯成不同的字符。
有一種編碼能夠把所有的符號都納入其中的方式,就是我們熟悉的 Unicode。但是 Unicode 只是規(guī)定了符號對應(yīng)的二進制代碼,并沒有詳細明確這個二進制代碼應(yīng)該如何存儲。
比如 "大家好hello" 對應(yīng)的 Unicode 編碼以及轉(zhuǎn)成二進制的結(jié)果如下
大 5927 0101 1001 0010 0111
家 5bb6 0101 1011 1011 0110
好 597d 0101 1001 0111 1101
h 0068 0000 0000 0110 1000
e 0065 0000 0000 0110 0101
l 006c 0000 0000 0110 1100
l 006c 0000 0000 0110 1100
o 006f 0000 0000 0110 1111
對于英文字符如果統(tǒng)一采用中文字符這種方式去存儲,也就是用和中文字符一樣的步長去存儲英文字符,必然會有很大的浪費(前 8 位必為 0 )。
為了解決這個問題,就可以用 UTF-8,UTF-8 最大的一個特點,就是它是一種變長的編碼方式。它可以使用 1~4 個字節(jié)表示一個符號,根據(jù)不同的符號而變化字節(jié)長度。這里簡單說一下 UTF-8 的規(guī)則:
- 單字節(jié)的字符,字節(jié)的第一位設(shè)為
0,對于英語文本,UTF-8碼只占用一個字節(jié),和ASCII碼完全相同;
- 單字節(jié)的字符,字節(jié)的第一位設(shè)為
-
n個字節(jié)的字符(n>1),第一個字節(jié)的前n位設(shè)為1,第n+1位設(shè)為0,后面字節(jié)的前兩位都設(shè)為10,這n個字節(jié)的其余空位填充該字符Unicode碼,高位用0補足。
-
大 11100101 10100100 10100111
家 11100101 10101110 10110110
好 11100101 10100101 10111101
h 0110 1000
e 0110 0101
l 0110 1100
l 0110 1100
o 0110 1111
對于 Swift 來說, String 是一系列字符的集合,也就意味著 String 中的每一個元素是不等?的。就是說在進行內(nèi)存移動的時候步?是不一樣的。這里和 Array 數(shù)組不一樣,當我們遍歷數(shù)組中的元素的時候,因為每個元素的內(nèi)存大小是一致的,所以每次的偏移量就是數(shù)組元素的內(nèi)存大小( Int 類型就偏移 8 字節(jié))。
但是對于 String 來說如果我要訪問 string[1] 那么是不是要把 "大" 這個字段遍歷完成之后才能夠確定 "家" 的偏移量? 依次內(nèi)推每一次都要重新遍歷計算偏移量,這個時候無疑增加了很多的內(nèi)存消耗。這就是為什么不能通過 Int 作為下標來去訪問 String。
2.2 Swift.Index 的本質(zhì)
來到源碼中關(guān)于 String 的 Index 布局的描述

從注釋中我們大致明白了上述表示的意思:
-
position aka encodedOffset:一個48 bit值,用來記錄碼位偏移量。 -
transcoded offset: 一個2 bit的值,用來記錄字符使用的碼位數(shù)量。 -
grapheme cache: 一個6 bit的值,用來記錄下一個字符的邊界。 -
reserved:7 bit的預(yù)留字段 -
scalar aligned: 一個1 bit的值,用來記錄標量是否已經(jīng)對齊過。
所以對于 String 的 Index 的本質(zhì)是存儲了 encodedOffset 和 transcoded offset。當我們構(gòu)建 String 的 Index 的時候,其實是把 encodedOffset 和 transcoded offset 計算出來存放到 Index 的內(nèi)存信息里面。而這個 Index 本身就是一個 64 位的位域信息。
