探究Swift的String底層實現

語言: CN / TW / HK

問題: Swift中的字串在記憶體中是如何儲存的?

  • 首先我們來程式碼驗證一下最簡單的字串--"空字串"的記憶體分佈 var empty = "" print("empty \(empty)") print(withUnsafePointer(to: &empty, { $0 }))

列印結果:

image.png

1. 檢視Swift.String原始碼連結, 搜尋"empty"

image.png

  • 看關鍵程式碼發現底部的public init方法呼叫了內部的init方法, 該初始化方法接收了一個_StringGuts的物件作為入參.
  • 原始碼357行開始, 也可看到結構體String持有_StringGuts作為成員變數. 我們繼續深挖StringGuts.

2. 檢視StringGuts.swift原始碼連結, 搜尋"empty".

  • _StringGuts結構體的初始化方法中, 可以看到該結構體持有StringObject作為成員變數, 撥雲見日, 深挖"StringObject". image.png

3. 檢視StringObject原始碼連結, 搜尋"empty".

  • StringObjectinit(count: variant: discirminator: flags:)方法中可以看到StringObject結構體的4個成員變數. image.png

  • 在建立一個字串的過程中, 都儲存了什麼內容呢? image.png

  • 從上面的原始碼可以看到String結構體在底層儲存的就是以上4個成員變數的內容.

  • 初始化方法中discriminator成員變數對應的的Nibbles又是什麼呢? image.png

  • 可以看到Nibbles也是一個列舉型別, 但是這裡只是定義, 原始定義是:

image.png - 可以看到, 這裡呼叫的方法判斷是如果當前是ASCII碼, 那麼當前的Discriminator判別器就是0xE000_0000_0000_0000, 如果不是ASCII碼就是0xA000_0000_0000_0000 image.png

  • 檢視一個空字串的記憶體分佈, 列印結果如下: image.png
  • 檢視一個包含中文的字串, 列印結果如下: image.png
  • ⭐️綜上, 我們可以看出A, E在這裡是用來標識當前是否是ASCII碼, 其中後面的數字是用來代表當前的多少個字串的長度.

  • _discriminator佔據4位, 每一位的標識如下: image.png

image.png

  • 大字串的規則和Nibbles的佈局結構如下: image.png

image.png

  • 對於原生的Swift字串來說, 採取的是tail-allocated(尾遞迴)儲存, 也就是在當前例項分配有超出其最後儲存屬性的額外空間, 額外的空間可用於直接在例項中儲存任意資料, 無需額外的堆分配.程式碼驗證如下:

image.png - 接下來我們需要關注的是0x8000000100003f60這個值, 根據上面的原始碼分析閱讀, 我們知道當前0x8標識的是大字串, 這點我們在原始碼裡也可以找到答案: image.png - 同時結合nibbles在記憶體中的佈局我們知道其中b60:b0是儲存字串的地址, 當然這個地址要加上偏移量, 這個偏移量是32, 這裡我們可通過計算器來驗證一下:

image.png

下面是ASCII碼十六進位制符號對照表:

image.png

  • 那麼前面的8個位元組是什麼呢? 我們可以先從初始化的流程來分析:

image.png

image.png

image.png - 所以可以看到, 除了我們當前的地址和標識位之外, 剩餘的就是countAndFlags, 這裡我們可以看到佈局如下:

image.png

image.png - 第一個標誌位是isASCII, 如果我們修改成中文, 這裡就會改變

image.png

  • ⭐️綜上, 我們可以發現Small strings(長度小於等於15的小字串)直接存在記憶體中, Large strings(長度大於15的大字串)儲存的是記憶體地址

回答: Small strings(長度小於等於15的小字串)直接存在記憶體中, Large strings(長度大於15的大字串)儲存的是記憶體地址

發文不易, 喜歡點讚的人更有好運氣👍 :), 定期更新+關注不迷路~

ps:歡迎加入筆者18年建立的研究iOS稽核及前沿技術的三千人扣群:662339934,坑位有限,備註“掘金網友”可被群管通過~