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

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

空字符串

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

string_0.jpg

從圖中可以看到,String 內部有個 _StringGuts_StringGuts 內部有個 _StringObject,_StringObject 內部有個 Builtin.BridgeObject 類型的_object 和一個 UInt64 類型的 _countAndFlagsBits

找到 StringGuts 源碼,可以看到 _StringGuts 是一個結構體,里面有個 _StringObject 類型的成員 _object,跟上面 Xcode 打印的一致。

string_1.jpg

搜索關鍵詞 empty,可以輕松找到創(chuàng)建空字符串的初始化方法:調用 _StringObject的方法empty:() 生成一個空的 _StringObject 對象后傳入到自身默認初始化方法:init(_ object: _StringObject) 中。

進一步查看 StringObject 源碼,同樣搜索關鍵詞 empty,找到方法:init(empty:())。因為我們的設備是64位,所以這個方法會進入到第一個分支中,分別初始化成員:_countAndFlagsBits_object(也跟圖一的打印保持一致)。

string_2.jpg

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  ................
*/

當然,我們也可以打斷點查看

string_3.jpg

以上我們可以看到,字符串其中一個字節(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é)沒有直接保存字符串內容,那么很有可能其中一部分內容保存著字符串的地址。帶著這種猜想,我們結合斷點跟匯編一起分析。

string_4.jpg

通過第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é)保存著字符串內容的地址等信息。

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
  }

我們也可以通過工具MachOView直接查看字符串 0123456789ABCDEFMach-O 文件中的位置。在__TEXT__cstring中(常量區(qū))找到了它。

string_6.jpg

StringObject

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

string_7.jpg

接下來簡化一下結構體_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中有三個成員:immortalnativebridged。很顯然這些取值會影響鑒別器 _discriminator 的狀態(tài)。
string_8.jpg

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

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

string_10.jpg

這里開始介紹小字符串和空字符串(很顯然,空字符串是一種特殊的小字符串)。

  • 64位平臺,小端模式,第一個字符存儲在最低位,高位字符和計數(shù)器存儲在高地址。例如最初的字符串1,0x0000000000000031 0xe100000000000000
  • 32位平臺,存儲空間變少,但仍然采用類似布局
string_11.jpg

這里是非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
  • 在我們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。其中_countAndFlagsBits 8字節(jié)中的高5位是標志位,即b63:b59b47:b0存儲著大字符串的長度 count;剩下的 b58:b48是保留位。而 _object 8字節(jié)的高4位是標志位,即b63:b60;剩下60位間接存儲大字符串內容的地址address(字符串的真實地址 = address + 偏移nativeBias,64位平臺是32,32位平臺是20,16位平臺是12)。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容