今天我們通過查看內存、匯編以及 Swift 源碼等多途徑來探究一下 Swift 中的 String 的內存布局及底層實現(xiàn)。
空字符串
首先創(chuàng)建一個最簡單的字符串,空字符串str1:

從圖中可以看到,String 內部有個 _StringGuts,_StringGuts 內部有個 _StringObject,_StringObject 內部有個 Builtin.BridgeObject 類型的_object 和一個 UInt64 類型的 _countAndFlagsBits。
找到 StringGuts 源碼,可以看到 _StringGuts 是一個結構體,里面有個 _StringObject 類型的成員 _object,跟上面 Xcode 打印的一致。

搜索關鍵詞 empty,可以輕松找到創(chuàng)建空字符串的初始化方法:調用 _StringObject的方法empty:() 生成一個空的 _StringObject 對象后傳入到自身默認初始化方法:init(_ object: _StringObject) 中。
進一步查看 StringObject 源碼,同樣搜索關鍵詞 empty,找到方法:init(empty:())。因為我們的設備是64位,所以這個方法會進入到第一個分支中,分別初始化成員:_countAndFlagsBits 和 _object(也跟圖一的打印保持一致)。

Nibbles 是個枚舉,源碼中給它加了多個extension。進一步查看源碼可以看到 Nibbles.emptyString,調用方法:small(isASCII: Bool)
enum 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 {
// Discriminator for small strings
@inlinable @inline(__always)
internal static func small(isASCII: Bool) -> UInt64 {
#if os(Android) && arch(arm64)
return isASCII ? 0x00E0_0000_0000_0000 : 0x00A0_0000_0000_0000
#else
return isASCII ? 0xE000_0000_0000_0000 : 0xA000_0000_0000_0000
#endif
}
非空字符串
由上面的分析,我們可以猜想到一個字符串變量至少占用了16字節(jié)。用 MemoryLayout 工具進行驗證也確實是16個字節(jié)。
var str1 = ""
print(MemoryLayout.size(ofValue: str1)) //16
小字符串
那么這16個字節(jié)是如何分配的呢?先把空字符 str1 的內容稍微改為:"1",并且借助 MJ 的內存小工具Mems 直接打印變量地址及內容
var str1 = "1"
print(Mems.ptr(ofVal: &str1))
print(Mems.memStr(ofVal: &str1))
/*
0x000000010000c1c8
0x0000000000000031 0xe100000000000000
(lldb) x 0x000000010000c1c8
0x10000c1c8: 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 e1 1...............
0x10000c1d8: 00 00 00 00 00 00 00 00 ff ff ff ff ff ff ff ff ................
*/
當然,我們也可以打斷點查看

以上我們可以看到,字符串其中一個字節(jié)內容為0x31("1"的十六進制ASCII值)。此時(Builtin.BridgeObject) _object = 0xe100000000000000,對比之前空字符串的(Builtin.BridgeObject) _object = 0xe000000000000000,我們可以猜想_object的其中一位可能存放字符串的長度。帶著這種猜想,我們去進一步驗證一下。
var str1 = "0123456789ABCDE"
print(Mems.memStr(ofVal: &str1))
//0x3736353433323130 0xef45444342413938
我們發(fā)現(xiàn)當字符串的長度不超過15時,打印結果跟猜想的一致。
大字符串
var str1 = "0123456789ABCDEF"
print(Mems.memStr(ofVal: &str1))
//0xd000000000000010 0x8000000100007930
當字符串長度大于15時,打印結果顯示前8個字節(jié)的其中一個字節(jié)內容為:0x10,也就是字符串的長度16,后8個字節(jié)內容為:0x8000000100007930。16個字節(jié)沒有直接保存字符串內容,那么很有可能其中一部分內容保存著字符串的地址。帶著這種猜想,我們結合斷點跟匯編一起分析。

通過第5、6行匯編計算得到字符串內容地址0x100007950,并讀取內容,確實存放著0123456789ABCDEF。斷點處也可以看到(Builtin.BridgeObject) _object = 0x8000000100007930,也就是說字符串變量地址的其中8個字節(jié)內容的恰好是(Builtin.BridgeObject) _object的地址。
不難發(fā)現(xiàn)0x8000000100007930的后面一串跟0x100007950很接近。是否存在某種聯(lián)系呢?
0x100007950 = 0x100007930 + 0x20
0x20,即十進制的32,其實就是地址偏移。這點在源碼中可以找到。綜上,我們可以初步得出結論,當字符串長度大于15時,字符串變量的其中8個字節(jié)保存著字符串長度等信息,另外8個字節(jié)保存著字符串內容的地址等信息。

extension _StringObject {
@inlinable @inline(__always)
internal static var nativeBias: UInt {
#if _pointerBitWidth(_64)
return 32
#elseif _pointerBitWidth(_32)
return 20
#elseif _pointerBitWidth(_16)
// TODO: we need to revisit all of this when we decide on efficient
// structures for storing String on 16-bit platforms
return 12
#else
#error("Unknown platform")
#endif
}
我們也可以通過工具MachOView直接查看字符串 0123456789ABCDEF 在 Mach-O 文件中的位置。在__TEXT的__cstring中(常量區(qū))找到了它。

StringObject
繼續(xù)查看 StringObject的源碼,一步步撕開它的面紗。開宗明義,注釋直接說明StringObject抽象了 String struct 位級別的解釋和創(chuàng)建。在64位平臺上,有個4位的重要鑒別器。標識是否小字符串、大字符串、橋接、ASCII、原生、共享、外來等。

接下來簡化一下結構體_StringObject中主要內容:
struct _StringObject {
enum Nibbles {}
struct CountAndFlags {
var _storage: UInt64
}
#if $Embedded
public typealias AnyObject = Builtin.NativeObject
#endif
#if _pointerBitWidth(_64)
var _countAndFlagsBits: UInt64
var _object: Builtin.BridgeObject
#elseif _pointerBitWidth(_32) || _pointerBitWidth(_16)
enum Variant {
case immortal(UInt)
case native(AnyObject)
case bridged(_CocoaString)
}
var _count: Int
var _variant: Variant
var _discriminator: UInt8
var _flags: UInt16
var _countAndFlagsBits: UInt64
#else
#error("Unknown platform")
#endif
}
由上我們可以看到:
- 在64位平臺,
_StringObject中有_countAndFlagsBits和_object兩個成員。允許小字符串內容自然地矢量對齊。 - 在32或者16位平臺,
_StringObject中有一個枚舉_variant成員和_count、_discriminator、_flags、_countAndFlagsBits。 - 枚舉
Variant中有三個成員:immortal、native、bridged。很顯然這些取值會影響鑒別器_discriminator的狀態(tài)。

第201行-325行主要根據平臺對 _StringObject 進行相應的初始化操作。后面開始介紹大字符串,也就是第341行開始,第二小節(jié)第4張圖片中所示。
- 大字符串可以是:原生的、共享的和外來的
- 原生字符串具有尾部分配的存儲空間,該存儲空間從偏移量開始
nativeBias來自存儲對象的地址 - 大字符串字面量存儲在常量區(qū),這點在上面
MachOView小節(jié)也得到了驗證 - 原生字符串始終由
Swift運行時管理 - 共享字符串沒有尾部分配的存儲,但提供對連續(xù)
UTF-8的訪問 - 外來字符串無法提供對連續(xù)
UTF-8的訪問。外來字符串僅包含不能被視為“共享”的延遲橋接的NSString,可以提供對UTF-16的訪問 - 8 字節(jié)存儲
_object,其中b63:b60用于存儲鑒別器,剩下60位存儲大字符串內容的地址(真實地址還需要加上偏移,64位平臺是0x20)。如上面字符串0123456789ABCDEF的0x8000000100007930中的8為鑒別器,剩下的都為地址,字符串真實地址 = 0x0000000100007930 + 0x20

這段及后續(xù)部分主要介紹鑒別器的一些工作,使用掩碼、位操作等技術(ObjC的runtime中常見這種操作),鑒別小字符串、大字符串、原生字符串、外來字符串、providesFastUTF8、橋接字符串等。

這里開始介紹小字符串和空字符串(很顯然,空字符串是一種特殊的小字符串)。
- 64位平臺,小端模式,第一個字符存儲在最低位,高位字符和計數(shù)器存儲在高地址。例如最初的字符串
1,0x0000000000000031 0xe100000000000000 - 32位平臺,存儲空間變少,但仍然采用類似布局

這里是非small,也就是大字符串的布局:
-
_object和非對象部分對半共享一個字的存儲單元,也就是說各8個字節(jié) - 非對象部分的8個字節(jié),高5位,即
b63:b59是標志位,b58:b48是保留位,剩下部分存儲著字符串的長度
源碼剩余部分主要是一些初始化器、查詢器、訪問器、前置檢查、輔助器以及聚合查詢與抽象等。
總結
-
struct String->struct _StringGuts->struct _StringObject- 64位平臺,
_StringObject包含_countAndFlagsBits和_object - 32及16位平臺,
_StringObject包含_count、_variant、_discriminator、_flags和_countAndFlagsBits
- 64位平臺,
在我們iOS開發(fā)中,一個
Swift字符串變量占用16個字節(jié)內存當字符串的長度
count<= 15 時,即small string,字符串變量地址的前15個字節(jié)直接存儲著字符串內容,后一個字節(jié)的高4位,存儲著一些標志位,低4位存儲著字符串的長度count當字符串的長度
count> 15 時,即large string,字符串變量地址的其中8個字節(jié)存儲著_countAndFlagsBits,8個字節(jié)存儲著_object。其中_countAndFlagsBits8字節(jié)中的高5位是標志位,即b63:b59;b47:b0存儲著大字符串的長度count;剩下的b58:b48是保留位。而_object8字節(jié)的高4位是標志位,即b63:b60;剩下60位間接存儲大字符串內容的地址address(字符串的真實地址 =address+ 偏移nativeBias,64位平臺是32,32位平臺是20,16位平臺是12)。