今天我們通過(guò)查看內(nèi)存、匯編以及 Swift
源碼等多途徑來(lái)探究一下 Swift
中的 String
的內(nèi)存布局及底層實(shí)現(xiàn)沙热。
空字符串
首先創(chuàng)建一個(gè)最簡(jiǎn)單的字符串,空字符串str1
:
從圖中可以看到,String
內(nèi)部有個(gè) _StringGuts
爵川,_StringGuts
內(nèi)部有個(gè) _StringObject
寝贡,_StringObject
內(nèi)部有個(gè) Builtin.BridgeObject
類型的_object
和一個(gè) UInt64
類型的 _countAndFlagsBits
圃泡。
找到 StringGuts 源碼,可以看到 _StringGuts
是一個(gè)結(jié)構(gòu)體价说,里面有個(gè) _StringObject
類型的成員 _object
鳖目,跟上面 Xcode
打印的一致缤弦。
搜索關(guān)鍵詞 empty
甸鸟,可以輕松找到創(chuàng)建空字符串的初始化方法:調(diào)用 _StringObject
的方法empty:() 生成一個(gè)空的 _StringObject
對(duì)象后傳入到自身默認(rèn)初始化方法:init(_ object: _StringObject) 中抢韭。
進(jìn)一步查看 StringObject 源碼刻恭,同樣搜索關(guān)鍵詞 empty
扯夭,找到方法:init(empty:())交洗。因?yàn)槲覀兊脑O(shè)備是64位,所以這個(gè)方法會(huì)進(jìn)入到第一個(gè)分支中咆爽,分別初始化成員:_countAndFlagsBits
和 _object
(也跟圖一的打印保持一致)斗埂。
Nibbles
是個(gè)枚舉呛凶,源碼中給它加了多個(gè)extension
。進(jìn)一步查看源碼可以看到 Nibbles.emptyString
模闲,調(diào)用方法: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
}
非空字符串
由上面的分析围橡,我們可以猜想到一個(gè)字符串變量至少占用了16字節(jié)翁授。用 MemoryLayout
工具進(jìn)行驗(yàn)證也確實(shí)是16個(gè)字節(jié)收擦。
var str1 = ""
print(MemoryLayout.size(ofValue: str1)) //16
小字符串
那么這16個(gè)字節(jié)是如何分配的呢谍倦?先把空字符 str1
的內(nèi)容稍微改為:"1"昼蛀,并且借助 MJ 的內(nèi)存小工具Mems 直接打印變量地址及內(nèi)容
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 ................
*/
當(dāng)然叼旋,我們也可以打斷點(diǎn)查看
以上我們可以看到夫植,字符串其中一個(gè)字節(jié)內(nèi)容為0x31("1"的十六進(jìn)制ASCII值)。此時(shí)(Builtin.BridgeObject) _object = 0xe100000000000000
延欠,對(duì)比之前空字符串的(Builtin.BridgeObject) _object = 0xe000000000000000
由捎,我們可以猜想_object
的其中一位可能存放字符串的長(zhǎng)度饿凛。帶著這種猜想,我們?nèi)ミM(jìn)一步驗(yàn)證一下碌宴。
var str1 = "0123456789ABCDE"
print(Mems.memStr(ofVal: &str1))
//0x3736353433323130 0xef45444342413938
我們發(fā)現(xiàn)當(dāng)字符串的長(zhǎng)度不超過(guò)15時(shí)蒙畴,打印結(jié)果跟猜想的一致膳凝。
大字符串
var str1 = "0123456789ABCDEF"
print(Mems.memStr(ofVal: &str1))
//0xd000000000000010 0x8000000100007930
當(dāng)字符串長(zhǎng)度大于15時(shí)蹬音,打印結(jié)果顯示前8個(gè)字節(jié)的其中一個(gè)字節(jié)內(nèi)容為:0x10
,也就是字符串的長(zhǎng)度16劫狠,后8個(gè)字節(jié)內(nèi)容為:0x8000000100007930
独泞。16個(gè)字節(jié)沒(méi)有直接保存字符串內(nèi)容苔埋,那么很有可能其中一部分內(nèi)容保存著字符串的地址组橄。帶著這種猜想玉工,我們結(jié)合斷點(diǎn)跟匯編一起分析。
通過(guò)第5削罩、6行匯編計(jì)算得到字符串內(nèi)容地址0x100007950
,并讀取內(nèi)容愿阐,確實(shí)存放著0123456789ABCDEF
缨历。斷點(diǎn)處也可以看到(Builtin.BridgeObject) _object = 0x8000000100007930
,也就是說(shuō)字符串變量地址的其中8個(gè)字節(jié)內(nèi)容的恰好是(Builtin.BridgeObject) _object
的地址丛肮。
不難發(fā)現(xiàn)0x8000000100007930
的后面一串跟0x100007950
很接近宝与。是否存在某種聯(lián)系呢冶匹?
0x100007950 = 0x100007930 + 0x20
0x20
嚼隘,即十進(jìn)制的32飞蛹,其實(shí)就是地址偏移。這點(diǎn)在源碼中可以找到豌汇。綜上拒贱,我們可以初步得出結(jié)論佛嬉,當(dāng)字符串長(zhǎng)度大于15時(shí)暖呕,字符串變量的其中8個(gè)字節(jié)保存著字符串長(zhǎng)度等信息湾揽,另外8個(gè)字節(jié)保存著字符串內(nèi)容的地址等信息。
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
}
我們也可以通過(guò)工具MachOView
直接查看字符串 0123456789ABCDEF
在 Mach-O
文件中的位置。在__TEXT
的__cstring
中(常量區(qū))找到了它诱告。
StringObject
繼續(xù)查看 StringObject
的源碼精居,一步步撕開(kāi)它的面紗靴姿。開(kāi)宗明義,注釋直接說(shuō)明StringObject
抽象了 String struct
位級(jí)別的解釋和創(chuàng)建宵晚。在64位平臺(tái)上坝疼,有個(gè)4位的重要鑒別器谆沃。標(biāo)識(shí)是否小字符串唁影、大字符串据沈、橋接、ASCII嗜诀、原生隆敢、共享、外來(lái)等崔慧。
接下來(lái)簡(jiǎn)化一下結(jié)構(gòu)體_StringObject
中主要內(nèi)容:
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位平臺(tái)拂蝎,
_StringObject
中有_countAndFlagsBits
和_object
兩個(gè)成員。允許小字符串內(nèi)容自然地矢量對(duì)齊惶室。 - 在32或者16位平臺(tái)温自,
_StringObject
中有一個(gè)枚舉_variant
成員和_count
、_discriminator
皇钞、_flags
悼泌、_countAndFlagsBits
鹅士。 - 枚舉
Variant
中有三個(gè)成員:immortal
券躁、native
、bridged
掉盅。很顯然這些取值會(huì)影響鑒別器_discriminator
的狀態(tài)也拜。
第201行-325行主要根據(jù)平臺(tái)對(duì) _StringObject
進(jìn)行相應(yīng)的初始化操作。后面開(kāi)始介紹大字符串趾痘,也就是第341行開(kāi)始慢哈,第二小節(jié)第4張圖片中所示。
- 大字符串可以是:原生的永票、共享的和外來(lái)的
- 原生字符串具有尾部分配的存儲(chǔ)空間卵贱,該存儲(chǔ)空間從偏移量開(kāi)始
nativeBias
來(lái)自存儲(chǔ)對(duì)象的地址 - 大字符串字面量存儲(chǔ)在常量區(qū),這點(diǎn)在上面
MachOView
小節(jié)也得到了驗(yàn)證 - 原生字符串始終由
Swift
運(yùn)行時(shí)管理 - 共享字符串沒(méi)有尾部分配的存儲(chǔ)侣集,但提供對(duì)連續(xù)
UTF-8
的訪問(wèn) - 外來(lái)字符串無(wú)法提供對(duì)連續(xù)
UTF-8
的訪問(wèn)键俱。外來(lái)字符串僅包含不能被視為“共享”的延遲橋接的NSString
,可以提供對(duì)UTF-16
的訪問(wèn) - 8 字節(jié)存儲(chǔ)
_object
世分,其中b63:b60
用于存儲(chǔ)鑒別器编振,剩下60位存儲(chǔ)大字符串內(nèi)容的地址(真實(shí)地址還需要加上偏移,64位平臺(tái)是0x20
)臭埋。如上面字符串0123456789ABCDEF
的0x8000000100007930
中的8
為鑒別器踪央,剩下的都為地址,字符串真實(shí)地址 = 0x0000000100007930 + 0x20
這段及后續(xù)部分主要介紹鑒別器的一些工作瓢阴,使用掩碼畅蹂、位操作等技術(shù)(ObjC
的runtime
中常見(jiàn)這種操作),鑒別小字符串荣恐、大字符串液斜、原生字符串、外來(lái)字符串叠穆、providesFastUTF8
少漆、橋接字符串等。
這里開(kāi)始介紹小字符串和空字符串(很顯然痹束,空字符串是一種特殊的小字符串)检疫。
- 64位平臺(tái),小端模式祷嘶,第一個(gè)字符存儲(chǔ)在最低位屎媳,高位字符和計(jì)數(shù)器存儲(chǔ)在高地址。例如最初的字符串
1
论巍,0x0000000000000031 0xe100000000000000
- 32位平臺(tái)烛谊,存儲(chǔ)空間變少,但仍然采用類似布局
這里是非small嘉汰,也就是大字符串的布局:
-
_object
和非對(duì)象部分對(duì)半共享一個(gè)字的存儲(chǔ)單元丹禀,也就是說(shuō)各8個(gè)字節(jié) - 非對(duì)象部分的8個(gè)字節(jié),高5位,即
b63:b59
是標(biāo)志位双泪,b58:b48
是保留位持搜,剩下部分存儲(chǔ)著字符串的長(zhǎng)度
源碼剩余部分主要是一些初始化器、查詢器焙矛、訪問(wèn)器葫盼、前置檢查、輔助器以及聚合查詢與抽象等村斟。
總結(jié)
-
struct String
->struct _StringGuts
->struct _StringObject
- 64位平臺(tái)贫导,
_StringObject
包含_countAndFlagsBits
和_object
- 32及16位平臺(tái),
_StringObject
包含_count
蟆盹、_variant
孩灯、_discriminator
、_flags
和_countAndFlagsBits
- 64位平臺(tái)贫导,
在我們iOS開(kāi)發(fā)中逾滥,一個(gè)
Swift
字符串變量占用16個(gè)字節(jié)內(nèi)存當(dāng)字符串的長(zhǎng)度
count
<= 15 時(shí)峰档,即small string
,字符串變量地址的前15個(gè)字節(jié)直接存儲(chǔ)著字符串內(nèi)容匣距,后一個(gè)字節(jié)的高4位面哥,存儲(chǔ)著一些標(biāo)志位,低4位存儲(chǔ)著字符串的長(zhǎng)度count
當(dāng)字符串的長(zhǎng)度
count
> 15 時(shí)毅待,即large string
尚卫,字符串變量地址的其中8個(gè)字節(jié)存儲(chǔ)著_countAndFlagsBits
,8個(gè)字節(jié)存儲(chǔ)著_object
尸红。其中_countAndFlagsBits
8字節(jié)中的高5位是標(biāo)志位吱涉,即b63:b59
;b47:b0
存儲(chǔ)著大字符串的長(zhǎng)度count
外里;剩下的b58:b48
是保留位怎爵。而_object
8字節(jié)的高4位是標(biāo)志位,即b63:b60
盅蝗;剩下60位間接存儲(chǔ)大字符串內(nèi)容的地址address
(字符串的真實(shí)地址 =address
+ 偏移nativeBias
鳖链,64位平臺(tái)是32,32位平臺(tái)是20墩莫,16位平臺(tái)是12)芙委。