Swift進階-類與結構體
Swift-函數派發
Swift進階-屬性
Swift進階-指針
Swift進階-內存管理
Swift進階-TargetClassMetadata和TargetStructMetadata數據結構源碼分析
Swift進階-Mirror解析
Swift進階-閉包
Swift進階-協議
Swift進階-泛型
Swift進階-String源碼解析
Swift進階-Array源碼解析
創建壹個空的字符串發生了什麽?
這裏並不能看出String的內存結構。那麽接下來就借助 Swift源碼 的方式看看String在內存中到底是如何存儲的。
打開swift源碼 -> stdib裏的 String.swift
最直觀地可以看到 String 是壹個結構體,就是我們所說的值類型;它有壹個成員變量 _StringGuts
其中最後有壹個創建空字符串初始化方式 self.init(_StringGuts()) :
接下來看看這個 _StringGuts 到底是什麽東西?
同樣找到swift源碼 -> stdib裏的 StringGuts.swift
_StringGuts 也是壹個結構體,它有壹個成員變量是 _StringObject 類型的實例;
並且在最後是通過初始化出壹個 _StringObject 類型的實例來初始化 _StringGuts 的。
所以真正swift的 String 的實質就是 _StringObject 。接下來看看 _StringObject 到底是什麽玩意兒?
找到swift源碼 -> stdib裏的 StringObject.swift ,可以看到 _StringObject 是壹個結構體,再找到空字符串的初始化函數:
ps: 註意這裏初始化時的傳參,下面會說到這幾個成員
最終找到字符串最終初始化函數,該函數是對成員的初始化賦值,那麽只要搞懂這幾個成員是代表什麽意思,那就能搞清楚字符串的底層實質了。
_StringObject 存儲著壹些成員變量,文章最開始使用x/8g格式化輸出壹個空字符串對象empty的時候,那我猜測:輸出的內容應該就是 _StringObject 裏的_count、_variant、_discriminator、_flags。
internal var _variant: Variant 是壹個枚舉值,默認是immortal 0:
internal var _discriminator: UInt8 在初始化的時候傳遞了壹個Nibbles.emptyString( Nibbles 是壹個枚舉類型):
0xE000_0000_0000_0000 與文章最上面截圖相對應起來了:
那接下來我們就能測試壹下字符串了:
字符a的ASCII編碼是97,97的16進制是61,註意那個2的字節位的輸出
小於等於15個字符串時,會記錄字符串的位數。
對於小字符串(小於等於15個字符串)來說,是優先直接存到內存當中,無需另外分配內存空間的。(和NSString差不多類似)
接下來看看中文字符
中文字符不是ASCII編碼,壹個中文字符占據3個字節(24位),也是我們上面通過源碼分析得出的使用了 0xA000_0000_0000_0000
所以 _StringObject.Nibbles 是壹個識別器,去識別字符串是不是ASCII編碼。
對於大字符串(大於15個字符串)來說,原本的小字符串占據的15個字節已經不足以存儲字符串了,那就會發生改變:
來看看0x8000000000000000在源碼中出現的定義是壹個大原始字符串:
那剩下的 0x000000010000b860 到底是什麽東西呢?它是字符串的內存 相對地址;
那應該偏移多少呢?來看源碼裏的註解
意思是0x10000b860需要加上偏移量 nativeBias 即32,32的16進制是0x20:
0x10000b860 + 0x20 = 0x10000b880
在源碼註解裏找到大字符串標誌位
大字符串前8位就記錄著這些標誌位信息,0xd000000000000012就是大字符串前8位,拿到科學計算器裏看看標誌位:
所以count是0x12,轉換成10進制就是18,正好對應18個字符。
對於 String 來說,它並不支持通過下標的方式獲取字符
只能通過 String.Index 的方式來訪問
對於 Swift 來說, String 是壹系列字符的集合,也就意味著 String 中的每壹個元素是不等長的。那也就意味著我們在進行內存移動的時候步長是不壹樣的,什麽意思?
比如我們有壹個 Array 的數組(Int 類型),當我們遍歷數組中的元素的時候,因為每個元素的內存大小是壹致的,所以每次的偏移量就是 8 個字節。
但是對於字符串來說不壹樣,比如我要方位 str[1] 那麽我是不是要把我這個字段遍歷完成之後才能夠確定是的偏移量?
依次內推每壹次都要重新遍歷計算偏移量,這個時候無疑增加了很多的內存消耗。這就是為什麽我們不能通過 Int 作為下標來去訪問 String 。
可以很直觀的看到 Index 的定義:
position aka encodedOffset 壹個 48 bit 值,用來記錄碼位偏移量;
transcoded offset : 壹個 2 bit 的值,用來記錄字符使用的碼位數量;
grapheme cache : 壹個 6 bit 的值,用來記錄下壹個字符的邊界;
reserved : 7 bit 的預留字段;
scalar aligned : 壹個 1 bit 的值,用來記錄標量是否已經對齊過。
String.Index 的本質就是壹個64位的位域信息,這個位域信息展示的就是上面的解釋。
創建 String.Index 實際上就是通過 encodedOffset 或者 transcoded offset , encodedOffset 就是方便我們從內存中通過下標訪問到字符串。