深入探究Swift中String的內(nèi)存布局及底層實(shí)現(xiàn)

今天我們通過(guò)查看內(nèi)存、匯編以及 Swift 源碼等多途徑來(lái)探究一下 Swift 中的 String 的內(nèi)存布局及底層實(shí)現(xiàn)沙热。

空字符串

首先創(chuàng)建一個(gè)最簡(jiǎn)單的字符串,空字符串str1

string_0.jpg

從圖中可以看到,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 打印的一致缤弦。

string_1.jpg

搜索關(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(也跟圖一的打印保持一致)斗埂。

string_2.jpg

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)查看

string_3.jpg

以上我們可以看到夫植,字符串其中一個(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)跟匯編一起分析。

string_4.jpg

通過(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)容的地址等信息。

string_5.jpg
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直接查看字符串 0123456789ABCDEFMach-O 文件中的位置。在__TEXT__cstring中(常量區(qū))找到了它诱告。

string_6.jpg

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)等崔慧。

string_7.jpg

接下來(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券躁、nativebridged掉盅。很顯然這些取值會(huì)影響鑒別器 _discriminator 的狀態(tài)也拜。
string_8.jpg

第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)臭埋。如上面字符串0123456789ABCDEF0x8000000100007930中的8為鑒別器踪央,剩下的都為地址,字符串真實(shí)地址 = 0x0000000100007930 + 0x20
string_9.jpg

這段及后續(xù)部分主要介紹鑒別器的一些工作瓢阴,使用掩碼畅蹂、位操作等技術(shù)(ObjCruntime中常見(jiàn)這種操作),鑒別小字符串荣恐、大字符串液斜、原生字符串、外來(lái)字符串叠穆、providesFastUTF8少漆、橋接字符串等。

string_10.jpg

這里開(kāi)始介紹小字符串和空字符串(很顯然痹束,空字符串是一種特殊的小字符串)检疫。

  • 64位平臺(tái),小端模式祷嘶,第一個(gè)字符存儲(chǔ)在最低位屎媳,高位字符和計(jì)數(shù)器存儲(chǔ)在高地址。例如最初的字符串1论巍,0x0000000000000031 0xe100000000000000
  • 32位平臺(tái)烛谊,存儲(chǔ)空間變少,但仍然采用類似布局
string_11.jpg

這里是非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
  • 在我們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:b59b47: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)芙委。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市狂秦,隨后出現(xiàn)的幾起案子灌侣,更是在濱河造成了極大的恐慌,老刑警劉巖裂问,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件侧啼,死亡現(xiàn)場(chǎng)離奇詭異牛柒,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)痊乾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門皮壁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人符喝,你說(shuō)我怎么就攤上這事闪彼√鸸拢” “怎么了协饲?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)缴川。 經(jīng)常有香客問(wèn)我茉稠,道長(zhǎng),這世上最難降的妖魔是什么把夸? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任而线,我火速辦了婚禮,結(jié)果婚禮上恋日,老公的妹妹穿的比我還像新娘膀篮。我一直安慰自己,他們只是感情好岂膳,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布誓竿。 她就那樣靜靜地躺著,像睡著了一般谈截。 火紅的嫁衣襯著肌膚如雪筷屡。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,111評(píng)論 1 285
  • 那天簸喂,我揣著相機(jī)與錄音毙死,去河邊找鬼。 笑死喻鳄,一個(gè)胖子當(dāng)著我的面吹牛扼倘,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播除呵,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼再菊,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了竿奏?” 一聲冷哼從身側(cè)響起袄简,我...
    開(kāi)封第一講書(shū)人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎泛啸,沒(méi)想到半個(gè)月后绿语,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年吕粹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了种柑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡匹耕,死狀恐怖聚请,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情稳其,我是刑警寧澤驶赏,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站既鞠,受9級(jí)特大地震影響煤傍,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜嘱蛋,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一蚯姆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧洒敏,春花似錦龄恋、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至镊靴,卻和暖如春铣卡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背偏竟。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工煮落, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人踊谋。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓蝉仇,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親殖蚕。 傳聞我的和親對(duì)象是個(gè)殘疾皇子轿衔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容