String
我們先來思考String變數占用多少記憶體?
var str1 = "0123456789"
print(Mems.size(ofVal: &str1)) // 16
print(Mems.memStr(ofVal: &str1)) // 0x3736353433323130 0xea00000000003938
我們通過列印可以看到String變數占用了16個位元組,并且列印記憶體布局,前后各占用了8個位元組
下面我們再進行反匯編來觀察下

可以看到這兩句指令正是分配了前后8個位元組給了String變數
那String變數底層存盤的是什么呢?
我們通過上面看到String變數的16個位元組的值其實是對應轉成的ASCII碼值
ASCII碼表的地址:https://www.ascii-code.com

我們看上圖就可以得知,左邊對應的是0~9的十六進制ASCII碼值,又因為小端模式下高位元組放高地址,低位元組放低地址的原則,對比正是我們列印的16個位元組中存盤的資料
0x3736353433323130 0xea00000000003938
然后我們再看后8個位元組前面的e和a分別代表的是型別和長度
如果String的資料是直接存盤在變數中的,就是用e來標明型別,如果要是存盤在其他地方,就會用別的字母來表示
我們String字符的長度正好是10,所以就是十六進制的a
var str1 = "0123456789ABCDE"
print(Mems.size(ofVal: &str1)) // 16
print(Mems.memStr(ofVal: &str1)) // 0x3736353433323130 0xef45444342413938
我們列印上面這個String變數,發現表示長度的值正好變成了f,而后7個位元組也都被填滿了,所以也證明了這種方式最多只能存盤15個位元組的資料
這種方式很像OC中的Tagger Pointer的存盤方式
如果存盤的資料超過15個字符,String變數又會是什么樣呢?
我們改變String變數的值,再進行列印觀察
var str1 = "0123456789ABCDEF"
print(Mems.size(ofVal: &str1)) // 16
print(Mems.memStr(ofVal: &str1)) // 0xd000000000000010 0x80000001000079a0
我們發現String變數的記憶體占用還是16個位元組,但是記憶體布局已經完全不一樣了
這時我們就需要借助反匯編來進一步分析了

看上圖能發現最后還是會先后分配8個位元組給String變數,但不同的是在這之前會呼叫了函式,并將回傳值給了String變數的前8個位元組
而且分別將字串的值還有長度作為引數傳遞了進去,下面我們就看看呼叫的函式里具體做了什么


我們可以看到函式內部會將一個掩碼的值和String變數的地址值相加,然后存盤到String變數的后8個位元組中
所以我們可以反向計算出所存盤的資料真實地址值
0x80000001000079a0 - 0x7fffffffffffffe0 = 0x1000079C0
其實也就是一開始存盤到rdi中的值

通過列印真實地址值可以看到16個位元組確實都是存盤著對應的ASCII碼值
那么真實資料是存盤在什么地方呢?
通過觀察它的地址我們可以大概推測是在資料段,為了更確切的認證我們的推測,使用MachOView來直接查看在可執行檔案中這句代碼的真正存盤位置
我們找到專案中的可執行檔案,然后右鍵Show in Finder

然后右鍵通過MachOView的方式來打開

最終我們發現在代碼段中的字串常量區中

對比兩個字串的存盤位置
我們現在分別查看下這兩個字串的存盤位置是否相同
var str1 = "0123456789"
var str2 = "0123456789ABCDEF"
我們還是用MachOView來打開可執行檔案,發現兩個字串的真實地址都是放在代碼段中的字串常量區,并且相差16個位元組

然后我們再看列印的地址的前8個位元組
0xd000000000000010 0x80000001000079a0
按照推測10應該也是表示長度的十六進制,而前面的d就代表著這種型別
我們更改下字串的值,發現果然表示長度的值也隨之變化了
var str2 = "0123456789ABCDEFGH"
print(Mems.size(ofVal: &str2)) // 16
print(Mems.memStr(ofVal: &str2)) // 0xd000000000000012 0x80000001000079a0
如果分別給兩個String變數進行拼接會怎樣呢?
var str1 = "0123456789"
str1.append("G")
print(Mems.size(ofVal: &str1)) // 16
print(Mems.memStr(ofVal: &str1)) // 0x3736353433323130 0xeb00000000473938
var str2 = "0123456789ABCDEF"
str2.append("G")
print(Mems.size(ofVal: &str2)) // 16
print(Mems.memStr(ofVal: &str2)) // 0xf000000000000011 0x0000000100776ed0
我們發現str1的后8個位元組還有位置可以存放新的字串,所以還是繼續存盤在記憶體變數里
而str2的記憶體布局不一樣了,前8個位元組可以看出來型別變成f,字串長度也變為十六進制的11;而后8個位元組的地址很像堆空間的地址值
驗證String變數的存盤位置是否在堆空間
為了驗證我們的推測,下面用反匯編來進行觀察
我們在驗證之前先創建一個類的實體變數,然后跟進去在內部呼叫malloc的指令位置打上斷點
class Person { }
var p = Person()

然后我們先將斷點置灰,重新反匯編之前的Sting變數

然后將置灰的malloc的斷點點亮,然后進入

發現確實會進入到我們之前在呼叫malloc的斷點處,所以這就驗證了確實會分配堆空間記憶體來存盤String變數的值了
我們還可以用LLDB的指令bt來列印呼叫堆疊詳細資訊來查看

發現也是在呼叫完append方法之后就會進行malloc的呼叫了,從這一層面也驗證了我們的推測
那堆空間里存盤的str2的值是怎樣的呢?
然后我們過掉了append函式后,列印str2的地址值,然后再列印后8個位元組存放的堆空間地址值

其內部偏移了32個位元組后,正是我們String變數的ASCII碼值
總結
1.如果字串長度小于等于0xF(十進制為15), 字串內容直接存盤到字串變數的記憶體中,并以ASCII碼值的小端模式來進行存盤
第9個位元組會存盤字串變數的型別和字符長度
var str1 = "0123456789"
print(Mems.size(ofVal: &str1)) // 16
print(Mems.memStr(ofVal: &str1)) // 0x3736353433323130 0xeb00000000473938
進行字串拼接操作后
如果拼接后的字串長度還是小于等于0xF(十進制為15),存盤位置同未拼接之前
var str1 = "0123456789"
str1.append("ABCDE")
print(Mems.size(ofVal: &str1)) // 16
print(Mems.memStr(ofVal: &str1)) // 0x3736353433323130 0xef45444342413938
如果拼接后的字串長度大于0xF(十進制為15),會開辟堆空間來存盤字串內容
字串的地址值中,前8個位元組存盤字串變數的型別和字符長度,后8個位元組存盤著堆空間的地址值,堆空間地址 + 0x20可以得到真正的字串內容
堆空間地址的前32個位元組是用來存盤描述資訊的
由于常量區是程式運行之前就已經確定位置了的,所以拼接字串是運行時操作,不可能再回存放到常量區,所以直接分配堆空間進行存盤
var str1 = "0123456789"
str1.append("ABCDEF")
print(Mems.size(ofVal: &str1)) // 16
print(Mems.memStr(ofVal: &str1)) // 0xf000000000000010 0x000000010051d600
2.如果字串長度大于0xF(十進制為15),字串內容會存盤在__TEXT.cstring中(常量區)
字串的地址值中,前8個位元組存盤字串變數的型別和字符長度,后8個位元組存盤著一個地址值,地址值 & mask可以得到字串內容在常量區真正的地址值
var str2 = "0123456789ABCDEF"
print(Mems.size(ofVal: &str2)) // 16
print(Mems.memStr(ofVal: &str2)) // 0xd000000000000010 0x80000001000079a0
進行字串拼接操作后,同上面開辟堆空間存盤的方式
var str2 = "0123456789ABCDEF"
str2.append("G")
print(Mems.size(ofVal: &str2)) // 16
print(Mems.memStr(ofVal: &str2)) // 0xf000000000000011 0x0000000106232230
dyld_stub_binder
我們反匯編看到底層呼叫的String.init方法其實是動態庫里的方法,而動態庫在記憶體中的位置是在Mach-O檔案的更高地址的位置,如下圖所示

所以我們這里看到的地址值其實是一個假的地址值,只是用來占位的

我們再跟進發現其內部會跳轉到另一個地址,取出其存盤的真正需要呼叫的地址值去呼叫
下一個呼叫的地址值一般都是相差6個位元組
0x10000774e + 0x6 = 0x100007754
0x100007754 + 0x48bc(%rip) = 0x10000C010
最后就是去0x10000C010地址中找到需要呼叫的地址值0x100007858


然后一直跟進,最后會進入到動態庫的dyld_stub_binder中進行系結

最后才會真正進入到動態庫中的String.init執行指令,而且可以發現其真正的地址值非常大,這也能側面證明動態庫是在可執行檔案更高地址的位置

然后我們在執行到下一個String.init的呼叫

跟進去發現這是要跳轉的地址值就已經是動態庫中的String.init真實地址值了


這也說明了dyld_stub_binder只會執行一次,而且是用到的時候在進行呼叫,也就是延遲系結
dyld_stub_binder的主要作用就是在程式運行時,將真正需要呼叫的函式地址替換掉之前的占位地址
Array
我們來思考Array變數占用多少記憶體?
var array = [1, 2, 3, 4]
print(Mems.size(ofVal: &array)) // 8
print(Mems.ptr(ofVal: &array)) // 0x000000010000c1c8
print(Mems.ptr(ofRef: array)) // 0x0000000105862270
我們通過列印可以看到Array變數占用了8個位元組,其記憶體地址就是存盤在全域區的地址
然而我們發現其記憶體地址的存盤空間存盤的地址值更像一個堆空間的地址
Array變數存盤在什么地方呢?
帶著疑問我們還是進行反匯編來觀察下,并且在malloc的呼叫指令處打上斷點

發現確實呼叫了malloc,那么就證明了Array變數內部會分配堆空間

等執行完回傳值給到Array變數之后,我們列印Array變數存盤的地址值記憶體布局,發現其內部偏移32個位元組的位置存盤著元素1、2、3、4
我們還可以直接通過列印記憶體結構來觀察
var array = [1, 2, 3, 4]
print(Mems.memStr(ofRef: array))
//0x00007fff88a8dd18
//0x0000000200000003
//0x0000000000000004
//0x0000000000000008
//0x0000000000000001
//0x0000000000000002
//0x0000000000000003
//0x0000000000000004
我們調整一下元素數量,再列印觀察
var array = [Int]()
for i in 1...8 {
array.append(i)
}
print(Mems.memStr(ofRef: array))
//0x00007fff88a8e460
//0x0000000200000003
//0x0000000000000008
//0x0000000000000010
//0x0000000000000001
//0x0000000000000002
//0x0000000000000003
//0x0000000000000004
//0x0000000000000005
//0x0000000000000006
//0x0000000000000007
//0x0000000000000008
發現第3段8個位元組的位置也變成了8,等同我們添加的元素數量
而第4端8個位元組的位置變成了16,說明擴大了一倍,可以推測這里存盤的是容量的擴增
根據我們的反匯編和推測,Array變數的內部結構如下圖所示

Array、String的底層結構都更像是參考型別的資料結構,只是表層作為值型別來使用
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/275007.html
標籤:iOS
下一篇:細談Activity四種啟動模式
