目錄
- 1缴啡、背景
- 2砸讳、LLVM對(duì)alloc的優(yōu)化
- 3、對(duì)象內(nèi)存大小的影響因素
- 4诵叁、字節(jié)對(duì)齊
- 5雁竞、結(jié)構(gòu)體內(nèi)存對(duì)齊
- 6、malloc的分析探索
- 7拧额、對(duì)象內(nèi)部對(duì)齊與結(jié)構(gòu)體內(nèi)部對(duì)齊的差別與意義
- 8碑诉、總結(jié)
1. 背景
學(xué)習(xí)不迷茫,無阻我飛揚(yáng)侥锦!大家好我是Tommy进栽!今天我們繼續(xù)來對(duì)底層進(jìn)行探索,本章內(nèi)容會(huì)比較多恭垦,里面的可能有些知識(shí)不太好理解快毛,大家可以分小節(jié)進(jìn)行閱讀。廢話不說我們這就開始番挺!
2. LLVM對(duì)alloc的優(yōu)化
-
再次分析 alloc 流程:
- 通過上篇《Objective-C 底層對(duì)象研究-上》我們已經(jīng)對(duì)
alloc
的運(yùn)行流程進(jìn)行了梳理唠帝,但這里存在一個(gè)問題不知道大家是否發(fā)現(xiàn)了?就是我們通過符號(hào)斷點(diǎn)等方式發(fā)現(xiàn)玄柏,alloc最先是調(diào)用了objc_alloc
方法后再開始走調(diào)用流程的襟衰;(動(dòng)態(tài)分析)
- 但是我們通過源碼方式分析發(fā)現(xiàn)
alloc
調(diào)用的并不是objc_alloc
而是_objc_rootAlloc
函數(shù)(靜態(tài)分析)
,這又是什么原因呢粪摘?
- 我們這里不如大膽猜測(cè)一下瀑晒,OC里面的方法調(diào)用都離不開兩個(gè)東西
SEL
和IMP
绍坝,SEL
就是方法標(biāo)示,IMP
就是指向方法具體實(shí)現(xiàn)的指針苔悦,就好比一本書的目錄一樣轩褐,你需要先查到目錄的條目之后再根據(jù)對(duì)應(yīng)的頁碼找到具體內(nèi)容。OC是動(dòng)態(tài)語言SEL
和IMP
是可以進(jìn)行動(dòng)態(tài)改變的间坐,所以alloc
是存在被改變可能性的灾挨。
- 通過上篇《Objective-C 底層對(duì)象研究-上》我們已經(jīng)對(duì)
-
探索調(diào)用 objc_alloc 的原因:
- 經(jīng)過我們的分析我們已經(jīng)有了大致思路,那么我們就用過研究源碼來驗(yàn)證我們的分析是否正確竹宋。
- 首先我們先通過搜索
objc_alloc
看看是否有結(jié)果......
- 哈哈劳澄!發(fā)現(xiàn)搜索出來的內(nèi)容還是挺多的,但是不要怕,經(jīng)過我的逐一排查我定位到了這里(紅框處)蜈七。
- 從這段代碼我們就很明顯的發(fā)現(xiàn)了在
runtime
中alloc
的IMP
的的確確是被替換了秒拔,這個(gè)已經(jīng)證明我們分析的思路是正確的;
- 那么我們繼續(xù)看一下這個(gè)
fixupMessageRef
函數(shù)是在什么時(shí)候被調(diào)用的飒硅?繼續(xù)通過搜索來得出答案砂缩。
[圖片上傳失敗...(image-f21061-1625212644421)] - 經(jīng)過排查找到了
fixupMessageRef
函數(shù)是在_read_images
這個(gè)函數(shù)中被調(diào)用的。 - 再看
_read_images
方法上面的注釋:“對(duì)鏈接中的頭信息執(zhí)行初始化處理”三娩,應(yīng)該可以猜到_read_images
方法可能與DYLD加載Mach-O文件有一定關(guān)系庵芭。我們可以給map_images_nolock
下個(gè)符號(hào)斷點(diǎn),為啥呢雀监?因?yàn)?code>_read_images我測(cè)試了無法斷住双吆,根據(jù)方法上面的注釋得知是通過map_images_nolock
這個(gè)函數(shù)調(diào)用的,所以果斷試了一下可以斷住会前。
- 通過符號(hào)斷點(diǎn)驗(yàn)證了我們的想法的的確確是由dyld進(jìn)行調(diào)用的好乐。到此我們可以先做一個(gè)簡(jiǎn)單的梳理:
-
思路梳理:
- 1、通過分析確認(rèn)了
alloc
確實(shí)是在runtime
的源碼中有被替換的跡象瓦宜; - 2蔚万、通過
fixupMessageRef
這個(gè)方法名稱,我們可以理解程序在運(yùn)行時(shí)临庇,需要對(duì)alloc
等一些方法進(jìn)行修復(fù)處理反璃;那我們是不是可以理解成:不管當(dāng)前是否存在問題,alloc
方法始終都會(huì)被改動(dòng)調(diào)用objc_alloc
假夺; - 3淮蜈、
fixupMessageRef
方法是在_read_images
中被調(diào)用的,而_read_images
是在DYLD加載Mach-O文件時(shí)進(jìn)行加載的侄泽;Mach-O文件中會(huì)存在一個(gè)叫做符號(hào)列表的內(nèi)容,里面就會(huì)將App的方法存放到此表中蜻韭,當(dāng)DYLD加載時(shí)就會(huì)讀取列表進(jìn)行映射操作悼尾,而這個(gè)過程就叫做符號(hào)綁定(現(xiàn)在可以先這么簡(jiǎn)單的理解)
- 5柿扣、通過以上分析我們可以得知,alloc方法在運(yùn)行時(shí)會(huì)被進(jìn)行檢測(cè)闺魏,如果檢測(cè)沒有問題它依然還是調(diào)用
objc_alloc
未状,如果存在問題就通過fixupMessageRef
方法進(jìn)行修復(fù)處理,而處理結(jié)果依然是調(diào)用objc_alloc
析桥,這一點(diǎn)需要大家細(xì)品一下司草。 如果以上思路都明確之后,我們應(yīng)該會(huì)想到alloc方法在運(yùn)行時(shí)做的只是修復(fù)工作泡仗,那么其實(shí)真正對(duì)alloc方法進(jìn)行修改的并不是在運(yùn)行時(shí)埋虹,實(shí)際上可能還是在更底層進(jìn)行修改的娩怎,而只是在runtime層增加了修復(fù)的邏輯,很可能是蘋果出于嚴(yán)謹(jǐn)性的考慮,在這一步額外增加的一層保護(hù)(可能是為了防止開發(fā)人員通過hook等方式對(duì)alloc方法進(jìn)行修改吧搔驼!~)。
- 1、通過分析確認(rèn)了
-
在LLVM中探索原因:
- 想要探索LLVM我們需要下載
LLVM-project
這里是鏈接[LLVM-project下載]焊刹,建議使用VSCode
進(jìn)行打開枷颊。 - 下載完畢之后試試搜索
objc_alloc
看看有什么結(jié)果题造,我們點(diǎn)擊第一個(gè)結(jié)果就能發(fā)現(xiàn)這些線索;“當(dāng)此方法返回true時(shí)猾瘸,Clang將把某些選擇器的非超級(jí)消息發(fā)送轉(zhuǎn)換為對(duì)相應(yīng)入口點(diǎn)的調(diào)用”界赔,通過這條注釋以及下面的alloc => objc_alloc
例子我們就可以明白了,在編譯階段alloc
就已經(jīng)被進(jìn)行了轉(zhuǎn)換設(shè)置牵触。
[圖片上傳失敗...(image-541a7c-1625212644421)] - 我們繼續(xù)搜索
shouldUseRuntimeFunctionsForAlloc
函數(shù)看看調(diào)用邏輯淮悼,發(fā)現(xiàn)是在tryGenerateSpecializedMessageSend
函數(shù)中進(jìn)行調(diào)用的。
- 再搜索
tryGenerateSpecializedMessageSend
函數(shù)查看調(diào)用邏輯揽思,搜索后我們來到了GeneratePossiblySpecializedMessageSend
函數(shù)袜腥。
- 從代碼我們可以簡(jiǎn)要的看出,當(dāng)發(fā)送消息時(shí)會(huì)先判斷是否符合發(fā)送特殊消息的條件绰更,如果符合就嘗試通過特殊方式發(fā)送瞧挤,如果不滿足就按正常流程發(fā)送消息。按照這個(gè)邏輯就能得出一個(gè)結(jié)論了:
- 想要探索LLVM我們需要下載
-
小結(jié)論:
就是當(dāng)alloc()第一次執(zhí)行時(shí)儡湾,被LLVM按特殊消息發(fā)送來處理了特恬,底層將目標(biāo)轉(zhuǎn)換成了objc_alloc();objc_alloc執(zhí)行后第一次調(diào)用了callAlloc();
首次進(jìn)入callAlloc()后去執(zhí)行objc_msgSend的方法徐钠,又再一次調(diào)用了alloc()癌刽,但是這次LLVM是按正常方式進(jìn)行處理,發(fā)送給了_objc_rootAlloc();_objc_rootAlloc()執(zhí)行后第二次調(diào)用了callAlloc();然后開始對(duì)內(nèi)存進(jìn)行對(duì)象內(nèi)存的開辟工作直至完成显拜。
-
再次梳理alloc流程:
- 我在上篇《Objective-C 底層對(duì)象研究-上》中畫過一個(gè)
alloc
流程圖衡奥,在這幅圖中我們當(dāng)時(shí)發(fā)現(xiàn)callAlloc()
被執(zhí)行了2次,那么我們將我們今天探索得到的結(jié)果远荠,添加到這幅流程圖中進(jìn)行補(bǔ)完矮固,大家可以對(duì)比看一下就能了解callAlloc
為什么會(huì)被調(diào)用了2次
的真正原因了。
- 接下來我們可以在深入一點(diǎn)譬淳,查看一下底層是如何處理函數(shù)調(diào)用的档址,我們可以通過
tryGenerateSpecializedMessageSend
函數(shù)中對(duì)alloc
方法處理為例子,一步一步跟蹤邻梆,最終我們走到了下面圖片所示的位置守伸;通過上下傳參最終會(huì)通過Builder.CreateCall()
跟Builder.CreateInvoke()
進(jìn)行函數(shù)的指令調(diào)用;
- 通過對(duì)底層LLVM的探索浦妄,我們可以發(fā)現(xiàn)蘋果對(duì)一些重要方法尼摹,尤其是跟內(nèi)存有關(guān)的方法都進(jìn)行了類似
HOOK
方式的處理,這里猜測(cè)應(yīng)該是對(duì)這些方法進(jìn)行了一些監(jiān)測(cè)和監(jiān)控處理剂娄。到此本小節(jié)結(jié)束蠢涝。
- 我在上篇《Objective-C 底層對(duì)象研究-上》中畫過一個(gè)
3、對(duì)象內(nèi)存大小的影響因素
-
查看對(duì)象占用內(nèi)存的大小:
- 我們接下來探索一下對(duì)象在內(nèi)存中的大小阅懦,每個(gè)對(duì)象都是在執(zhí)行alloc后都會(huì)開辟出內(nèi)存空間惠赫;我們來看一下
ZXPerson
的對(duì)象在內(nèi)存中占用了多少空間,我們可以通過class_getInstanceSize()
方法打印大小故黑,使用此方法時(shí)請(qǐng)導(dǎo)入#import <objc/runtime.h>
頭文件儿咱。編譯運(yùn)行后顯示了占用大小。
- 我們接下來探索一下對(duì)象在內(nèi)存中的大小阅懦,每個(gè)對(duì)象都是在執(zhí)行alloc后都會(huì)開辟出內(nèi)存空間惠赫;我們來看一下
-
發(fā)現(xiàn)影響大小的因素:
-
增加屬性和成員變量:我們添加或者刪除一下屬性和成員變量可以觀察到场晶,對(duì)象的大小會(huì)有不同的不變化混埠,增加時(shí)大小會(huì)增大,反之亦然诗轻;
-
添加方法:屬性和變量會(huì)影響大小改變钳宪,我們也可以試試添加方法是否也會(huì)改變大小扳炬?答案是并不會(huì)吏颖。
- 到此我們可以得到一個(gè)結(jié)論:對(duì)象的內(nèi)存大小是由成員變量決定的,跟其他內(nèi)容沒有關(guān)系恨樟。
-
-
class_getInstanceSize()方法:
- 我們進(jìn)入到objc源碼
Command+shift+O
搜索class_getInstanceSize
直接就可以定位到半醉。
- 我們一步一步定位到這里給出了明確提示:May be unaligned depending on class's ivars.
- 我們進(jìn)入到objc源碼
-
沒有變量時(shí)打印為什么是8?:
- 當(dāng)我們將所有定義的成員變量刪除之后劝术,通過
class_getInstanceSize()
方法打印結(jié)果是8
缩多,這也就說明我們一定從父類中繼承過來了成員變量呆奕,我們?cè)偻ㄟ^源碼進(jìn)行驗(yàn)證。
- 我們直接搜索父類
NSObject
衬吆,就會(huì)看到父類中存在一個(gè)變量叫做isa
梁钾;那么第一個(gè)疑問就解開了,確實(shí)從父類中繼承了變量過來逊抡;那么大小為什么是8
呢姆泻?我們繼續(xù)分析。 - 我們發(fā)現(xiàn)這個(gè)
isa
的類型是Class
冒嫡,我們跟蹤一下看看有什么結(jié)果麦射,Command+shift+O
搜索Class
,發(fā)現(xiàn)Class是一個(gè)類型定義灯谣,實(shí)際是objc_class
類型的指針類型,而在arm64
下一個(gè)指針正好是占用8
個(gè)字節(jié)蛔琅。
- 而
objc_class
是一個(gè)結(jié)構(gòu)體并且繼承objc_object
胎许,那么我們自定義的類在底層實(shí)際都變成了objc_object
。我們可以通過clang命令對(duì).m文件進(jìn)行編譯罗售。(我的實(shí)例程序都寫在了mian.m文件里辜窑,所以我就編譯了main.m文件)
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
- 編譯成
C++
文件我們就能看到我們定義的類在編譯之后都會(huì)變成objc_object
結(jié)構(gòu)體類型。
ps:這么做的目的是蘋果為了在底層對(duì)開發(fā)人員定義的類進(jìn)行統(tǒng)一處理而進(jìn)行了轉(zhuǎn)換寨躁,因?yàn)樘O果不可能在底層去逐一的去實(shí)現(xiàn)開發(fā)人員定義的類穆碎,這是不可能定義出來的,因?yàn)榭勺冃蕴罅酥翱遥凰詾榱朔奖銓?duì)類進(jìn)行管理和操作所禀,就必須設(shè)計(jì)一個(gè)通用的類型來替代。
通源碼探究我們發(fā)現(xiàn)
Object-C
的底層都是通過C/C++
來實(shí)現(xiàn)的放钦,所以OC
中的對(duì)象也會(huì)轉(zhuǎn)化成C/C++
中的某一個(gè)數(shù)據(jù)結(jié)構(gòu)色徘,到此本小結(jié)結(jié)束。 - 當(dāng)我們將所有定義的成員變量刪除之后劝术,通過
4操禀、字節(jié)對(duì)齊
通過上一節(jié)的研究褂策,我們得知
Object-C
的底層都是通過C/C++
來實(shí)現(xiàn)的,所以OC
中的對(duì)象也會(huì)轉(zhuǎn)化成C/C++
中的某一個(gè)數(shù)據(jù)結(jié)構(gòu)颓屑。-
我們?cè)俅位氐皆创a
_class_createInstanceFromZone()
里找到instanceSize()
斤寂,通過上一篇的探索我們已經(jīng)得知了,該方法是負(fù)責(zé)返回對(duì)象所需的空間大小的揪惦;我們跟蹤進(jìn)去可以看到優(yōu)先從緩存中查找大小遍搞,如果緩存沒有就重新計(jì)算大小,最后還有一個(gè)判斷就是如果計(jì)算的大小不足16字節(jié)
器腋,就補(bǔ)足16字節(jié)
尾抑。
-
從
alignedInstanceSize()
方法中我看可以看到底層系統(tǒng)將對(duì)象占用的內(nèi)存大小進(jìn)行了字節(jié)對(duì)齊歇父,我看通過word_align()
了解具體對(duì)齊算法。
-
算法解析:
-
WORD_MASK
的值是7UL
再愈,其實(shí)就是7
榜苫;(UL
的意思是unsignedLong
無符號(hào)長(zhǎng)整型); 假如x=7翎冲;(7+7) & ~7 垂睬;14 & ~7 乖寒;0000 1110 & 1111 1000 = 0000 1000(8)
假如x=9跳夭;(9+7) & ~7 ;16 & ~7 最仑;0001 0000 & 1111 1000 = 0001 0000(16)
- 我們可以看到算法其實(shí)是按
8字節(jié)
進(jìn)行對(duì)齊缴渊,不足8就按8算赏壹,超過8就以8的倍數(shù)進(jìn)行,例如9:就按8的2倍計(jì)算也就是16衔沼;如果是20就按8的3倍計(jì)算也就是24(大家可以自行驗(yàn)證) (ps:~7 是意思是非7 就是按7的二進(jìn)制取反)
-
-
字節(jié)對(duì)齊原理:
- 為什么要進(jìn)行字節(jié)對(duì)齊蝌借?這是為了提高
CPU
讀內(nèi)的效率將內(nèi)存統(tǒng)一按一個(gè)大小進(jìn)行對(duì)齊處理,實(shí)際占用的大小不足時(shí)指蚁,就通過補(bǔ)0
方式對(duì)齊菩佑。這么做雖然犧牲了一定的內(nèi)存空間,但是讀取的效率會(huì)大幅提升凝化,也就是用 “空間換時(shí)間”稍坯。
- 為什么要進(jìn)行字節(jié)對(duì)齊蝌借?這是為了提高
-
思路梳理:
- 我們定義的類從
NSObject
里集成了isa
屬性占用8
字節(jié); - 分析源碼
instanceSize()
得知對(duì)象內(nèi)部結(jié)構(gòu)是已8
字節(jié)進(jìn)行對(duì)齊搓劫,但系統(tǒng)是最小給分配了16
字節(jié)瞧哟; - 字節(jié)對(duì)齊算法:通過
(x + WORD_MASK) & ~WORD_MASK
方式進(jìn)行計(jì)算; - 為什么要選擇以
8
字節(jié)對(duì)齊?這是因?yàn)樵?code>arm64下枪向,8
字節(jié)基本上就是最大的占用字節(jié)數(shù)了绢涡。 - 如果對(duì)象大小超過
16
字節(jié)會(huì)怎么樣?其實(shí)在最后底層還會(huì)以16
字節(jié)進(jìn)行一次對(duì)齊處理遣疯,請(qǐng)看下一個(gè)小節(jié)內(nèi)容結(jié)構(gòu)體內(nèi)存對(duì)齊雄可。
- 我們定義的類從
5、結(jié)構(gòu)體內(nèi)存對(duì)齊
- 在上一篇我們通過
x/4gx
查看了類對(duì)象中在內(nèi)存中的存放狀態(tài)缠犀,其中我們發(fā)現(xiàn)了一個(gè)現(xiàn)象就是一個(gè)8字節(jié)的空間里面存放了2個(gè)不同的數(shù)據(jù)数苫,這種現(xiàn)象就叫做內(nèi)存對(duì)齊并且做了相關(guān)優(yōu)化處理。當(dāng)我們創(chuàng)建一個(gè)對(duì)象指針時(shí)辨液,該指針實(shí)際指向的是一個(gè)結(jié)構(gòu)體類型虐急,那么對(duì)于結(jié)構(gòu)體來說內(nèi)存大小這塊是否有什么不一樣?下面就讓我們來一起探究一番滔迈。 - 結(jié)構(gòu)體內(nèi)存的三個(gè)原則:
- 結(jié)構(gòu)體內(nèi)第一個(gè)成員以0為起始位置止吁,而后的成員起始位置要從成員的占用大小或子成員的占用大小的整數(shù)倍開始被辑;
- 如果內(nèi)部成員是一個(gè)結(jié)構(gòu)體,則結(jié)構(gòu)體成員要從其內(nèi)部最大元素占用大小的整數(shù)倍地址開始存儲(chǔ)敬惦;
- 構(gòu)體的總大小,也就是sizeof的結(jié)果,必須是其內(nèi)部最大成員的整數(shù)倍.不足的要補(bǔ)?盼理;
- 我們可以自己編寫2個(gè)結(jié)構(gòu)體來進(jìn)行驗(yàn)證:
-
內(nèi)部成員聲明位置先后不同,得到的大小不同俄删;出現(xiàn)這樣的原因就是根依據(jù)上面的三個(gè)原則而得到的結(jié)果宏怔,我們先來驗(yàn)證一下非嵌套的結(jié)構(gòu)體。
-
-
測(cè)試下帶嵌套的結(jié)構(gòu)體畴椰,我新建一個(gè)ZXStruct3臊诊,然后將ZXStruct1聲明為內(nèi)部的一個(gè)成員。
- 理解:
-
ZXStruct3
的第一個(gè)成員占用到第3
個(gè)字節(jié)位置斜脂,根據(jù)原則2
應(yīng)按照結(jié)構(gòu)內(nèi)部最大元素的大小的整數(shù)倍開始存儲(chǔ)抓艳,所以從8
開始;然后再用8 + zx_t1
大小帚戳,就可以直接得出實(shí)際大小了也就是8 + 24 = 32
玷或。 - 結(jié)論:先計(jì)算原結(jié)構(gòu)體占用大小,再根據(jù)
原則2
對(duì)齊销斟,最后加上嵌套結(jié)構(gòu)體就是最終的大小結(jié)果。
-
- 理解:
- 為何要對(duì)齊椒舵?帶來什么好處蚂踊?
- 結(jié)合我們上面介紹的字節(jié)對(duì)齊、和結(jié)構(gòu)體對(duì)齊的知識(shí)笔宿,我們就可以猜到對(duì)齊的原因就是為了提升讀取效率犁钟,蘋果在內(nèi)存讀取上做了優(yōu)化處理,請(qǐng)看下面的例子大家就能有所感悟了泼橘。
- 我們還是以
ZXStruct1
前三個(gè)成員為例涝动,將3個(gè)成員放大來觀察。
- 不采取對(duì)齊:
- 如果不按成員大小進(jìn)行對(duì)齊炬灭,就會(huì)安裝圖上所示的樣子進(jìn)行排序醋粟,最后再進(jìn)行補(bǔ)齊,但是讀取邏輯就發(fā)生變化了重归。
- 首先
8
位讀取米愿,p1
可以一次讀完,再次按8
位讀取的時(shí)候就發(fā)現(xiàn)無法正確讀取了鼻吮,因?yàn)榘l(fā)現(xiàn)后8
位包含了混合數(shù)據(jù)育苟,所以需要根據(jù)成員大小調(diào)整步長(zhǎng)讀取,共需要4次
完成椎木,這樣就會(huì)降低效率违柏。
- 采取對(duì)齊:
- 按成員大小進(jìn)行對(duì)齊后博烂,首先按
8
位讀取,p1
可以一次讀完漱竖,這個(gè)沒有發(fā)生改變禽篱,后面讀取時(shí)判斷含有混合數(shù)據(jù)的話,按數(shù)據(jù)中最大的占位進(jìn)行讀取闲孤,并且將補(bǔ)位的空位進(jìn)行合并谆级,(反正最后都需要補(bǔ)位,不如將空位移動(dòng)到前面一起讀取來提高效率)所以讀取3次
就可以完成了讼积。
- 按成員大小進(jìn)行對(duì)齊后博烂,首先按
- 至此結(jié)構(gòu)體內(nèi)存對(duì)齊的相關(guān)知識(shí)介紹完畢,最后附上一個(gè)各個(gè)類型所占用大小的列表圖肥照。
C OC 32位 64位 bool BOOL(64位) 1 1 signed char (_signed char)int8_t、BOOL(32位) 1 1 unsigned char Boolean 1 1 short int16_t 2 2 unsigned short unichar 2 2 int勤众、int32_t NSInterger(32位)舆绎、boolean_t(32位) 4 4 unsigned int NSUInterger(32位)、boolean_t(64位) 4 4 long NSInterger(64位) 4 8 unsigned long NSUInterger(64位) 4 8 long long int64_t 8 8 float CGFloat(32位) 4 4 double CGFloat(64位) 8 8
6们颜、malloc的分析探索
-
首先我們先來看一個(gè)現(xiàn)象吕朵,我對(duì)
ZXPerson
類的對(duì)象*zxp
分別通過class_getInstanceSize()
、sizeof()
窥突、malloc_size()
努溃、3
個(gè)函數(shù)進(jìn)行打印輸出;
-
此時(shí)我們ZXPerson類中定義了4個(gè)屬性再加上隱藏屬性isa阻问,一共是5個(gè)屬
-
class_getInstanceSize()
打印了32
梧税, 這個(gè)沒有問題(8+8+8+4+1 最后按8字節(jié)對(duì)齊 = 32)
; -
sizeof()
打印了8
称近,這個(gè)沒有問題(因?yàn)榇蛴〉氖侵羔樀诙樱羔樀拇笮【褪?占字節(jié))
; -
malloc_size()
打印了32
刨秆,跟class_getInstanceSize()
一樣凳谦,貌似也應(yīng)該沒有問題;
-
-
此時(shí)我們ZXPerson類中新增一個(gè)屬性zxNikeName再來看看結(jié)果衡未。
-
class_getInstanceSize()
打印了40
沒毛彩础!(8+8+8+4+1+8 最后按8字節(jié)對(duì)齊正好 = 40)
-
sizeof()
沒變化缓醋; - 而
malloc_size()
結(jié)果卻不同了變成了48
剔交,奇奇怪怪的事情就這樣神奇的發(fā)生了!那么為什么呢改衩?接下來我們來一起探索一下岖常。
-
-
首先我們先通過追蹤下
malloc_size()
,從注釋“Returns size of given ptr”
我們得知malloc_size()
函數(shù)會(huì)根據(jù)ptr
來返回大小值葫督,而ptr
就是我們傳入的指針竭鞍。當(dāng)我們想繼續(xù)往下追蹤時(shí)發(fā)現(xiàn)已經(jīng)無法往下走了板惑。那怎么辦呢?首先不要慌偎快!我們確定一下這個(gè)malloc_size()
函數(shù)的所在位置是在哪里冯乘,從上面的導(dǎo)航我們可以看到這個(gè)函數(shù)是在malloc
這個(gè)庫下面。我們就可以再通過源碼方式來進(jìn)行研究了(日后我們探究的思路都是以這個(gè)方式來進(jìn)行的)
晒夹。
-
在探索源碼前我們還可以去蘋果官網(wǎng)搜索這個(gè)函數(shù)的官方解釋
malloc_size
的蘋果官網(wǎng)解釋:“返回ptr所指向的分配的內(nèi)存塊的大小裆馒。內(nèi)存塊的大小總是至少和它的分配一樣大,也可能會(huì)更大”
丐怯,通過官方的解釋我們就能理解我們現(xiàn)在遇到的這個(gè)現(xiàn)象了吧喷好,現(xiàn)象就是返回的大小可能跟實(shí)際分配的一致或更大。那么接下來读跷,我們帶著這個(gè)問題來開始源碼的探索梗搅。
-
下載
libmalloc
可編譯的源碼:下載libmalloc可編譯的源碼
-
在上一篇文章中我們已經(jīng)對(duì)
alloc
的開辟流程進(jìn)行了梳理,發(fā)現(xiàn)alloc
申請(qǐng)內(nèi)存是calloc
發(fā)起的效览,所以我們直接把斷點(diǎn)斷到calloc
上无切。對(duì)于這塊不清楚的同學(xué)請(qǐng)走傳送門 《Objective-C 底層對(duì)象研究-上》
-
我們將斷點(diǎn)斷在
calloc
上,來跟蹤內(nèi)存開辟的機(jī)制丐枉,編譯-運(yùn)行后我們進(jìn)入到了calloc
里哆键,這只是一個(gè)封裝函數(shù),繼續(xù)跟蹤_malloc_zone_calloc()
瘦锹。
-
進(jìn)來后我們可以觀察一下籍嘹,根據(jù)上面的官方文檔的說明,我們只需關(guān)注
ptr
就可以了沼本,那么我們就定位到了1560
行噩峦。但是在想從1560
行往下走就走不到了(無論是搜索關(guān)鍵字锭沟,符號(hào)斷點(diǎn)都無法定位)
抽兆。仔細(xì)觀察后發(fā)現(xiàn)是通過zone
這個(gè)對(duì)象中calloc
的方法返回的,這時(shí)我們可以通過LLDB
命令po zone->calloc
進(jìn)行查看族淮,返回的結(jié)果就是實(shí)際調(diào)用辫红。
(這個(gè)zone->calloc其實(shí)可以理解成是一個(gè)賦值語句,從這個(gè)zone->calloc中獲取到相關(guān)的函數(shù)去執(zhí)行祝辣,當(dāng)搜索 “=zone->calloc”關(guān)鍵字時(shí)贴妻,會(huì)有好多類似的語句,都是用于從獲取賦值的)
-
我們搜索
default_zone_calloc()
找到位置發(fā)現(xiàn)又調(diào)用了zone
這個(gè)對(duì)象中calloc
的方法蝙斜,我們繼續(xù)po
它得到結(jié)果名惩。
-
我們?cè)賹ふ?code>nano_malloc.c文件的
878
行,根據(jù)分析我們可以分析出return p
是正確的路線孕荠,p
是通過_nano_malloc_check_clear()
函數(shù)返回的娩鹉,我們繼續(xù)就探索下去攻谁。
- 進(jìn)到
_nano_malloc_check_clear()
我們可以將復(fù)雜的方法簡(jiǎn)單化處理下,先將不重要的判斷隱藏掉弯予。
- 進(jìn)到
-
思路分析:
-
*ptr
從堆區(qū)開辟空間戚宦,如果ptr
沒有,就循環(huán)進(jìn)行查找锈嫩。segregated_next_block()
函數(shù)大家可以自己看一下受楼,內(nèi)部是一個(gè)while
死循環(huán),我這里不做過多介紹呼寸;(額……這里還是啰嗦一下吧艳汽,這個(gè)函數(shù)的功能就是在堆區(qū)不斷的進(jìn)行查找,找到合適的位置就分配存儲(chǔ)地址等舔,因?yàn)槎汛鎯?chǔ)是不是按序的骚灸,數(shù)據(jù)之間存在不規(guī)則的空隙,所以需要不斷的循環(huán)來進(jìn)行處理)
- 實(shí)際上由于
*ptr
是新開辟的慌植,所以最終還是會(huì)走到segregated_next_block()
這步甚牲,并將上面算好的slot_bytes
大小傳遞過來進(jìn)行開辟工作。 - 那么具體大小就是根據(jù)
segregated_size_to_fit()
函數(shù)進(jìn)行處理的了蝶柿,我們可以追蹤進(jìn)去丈钙。
-
-
追蹤到
segregated_size_to_fit()
后我們就看到了NANO_REGIME_QUANTA_SIZE
宏定義,追蹤進(jìn)去查看發(fā)現(xiàn)是讓1左移了4位也就是16交汤,最后再通過公式來進(jìn)行對(duì)齊運(yùn)算雏赦。//16字節(jié)對(duì)齊公式: k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM\ slot_bytes = k << SHIFT_NANO_QUANTUM;
-
算法解析:
-
NANO_REGIME_QUANTA_SIZE
的值是16
; - 假如
size=7芙扎;((7+15)>>4)<<4 星岗;(22>>4)<<4 ;0001 0110 >> 4 = 0000 0001 ; 0000 0001 << 4 = 0001 0000(16)
- 假如
size=32戒洼;((32+15)>>4)<<4 俏橘;(47>>4)<<4 ;0010 1111 >> 4 = 0000 0010 ; 0000 0010 << 4 = 0010 0000(32)
- 實(shí)際可以替換為:
slot_bytes = (size + NANO_REGIME_QUANTA_SIZE - 1) & ~ SHIFT_NANO_QUANTUM
-
到此就知道了用
malloc_size()
打印對(duì)象是48
的原因了圈浇,因?yàn)檫M(jìn)行了16
字節(jié)對(duì)齊寥掐。
7、對(duì)象內(nèi)部對(duì)齊與結(jié)構(gòu)體內(nèi)部對(duì)齊的差別與意義
- 對(duì)象中成員變量(結(jié)構(gòu)體內(nèi)部)采用
8
字節(jié)對(duì)齊磷蜀; - 對(duì)象與對(duì)象在堆內(nèi)存中采用
16
字節(jié)對(duì)齊召耘; - 為何不考慮都是用
8
字節(jié)對(duì)齊?- 原因1:拉伸對(duì)象與對(duì)象直接的內(nèi)存空隙褐隆,有效降低野指針內(nèi)存訪問帶來的問題污它。
- 原因2:由于我們的類都是繼承于
NSObject
,所以每個(gè)類默認(rèn)都會(huì)包含一個(gè)8
字節(jié)的isa
屬性,如果隨便增加1
個(gè)變量就已經(jīng)超過8
字節(jié)(也就是最少也是16
字節(jié)起步)衫贬,所以蘋果索性就按16
字節(jié)進(jìn)行對(duì)齊處理降低運(yùn)算次數(shù)蜜宪。
8、總結(jié)
- 通過了解LLVM對(duì)alloc的優(yōu)化處理祥山,我們探究了callAlloc調(diào)用2次的原因圃验,以及調(diào)用的流程;
- 對(duì)象中的屬性缝呕、成員變量是唯一影響大小的因素澳窑;
- 對(duì)象內(nèi)部屬性、成員變量是已8字節(jié)進(jìn)行對(duì)齊處理供常;
- 記住結(jié)構(gòu)體內(nèi)部對(duì)齊的三個(gè)原則摊聋;
- 對(duì)象在堆內(nèi)存中是以16字節(jié)進(jìn)行對(duì)齊的;
- 要理解對(duì)象內(nèi)部對(duì)齊與結(jié)構(gòu)體內(nèi)部對(duì)齊的差別與意義栈暇;
注:
- Clang: Clang 是一個(gè)
C語言
麻裁、C++
、Objective-C
語言的輕量級(jí)編譯器源祈。源代碼發(fā)布于BSD
協(xié)議下煎源。Clang
將支持其普通lambda
表達(dá)式、返回類型的簡(jiǎn)化處理以及更好的處理constexpr
關(guān)鍵字香缺。 - LLVM:
LLVM
是構(gòu)架編譯器(compiler)的框架系統(tǒng)手销,以C++
編寫而成,用于優(yōu)化以任意程序語言編寫的程序的編譯時(shí)間(compile-time)图张、鏈接時(shí)間(link-time)锋拖、運(yùn)行時(shí)間(run-time)以及空閑時(shí)間(idle-time),對(duì)開發(fā)者保持開放祸轮,并兼容已有腳本兽埃。
寫到最后
- 到此本篇內(nèi)容以及結(jié)束!如果您喜歡的話別忘了賞個(gè)贊适袜!您的點(diǎn)贊是我最大的動(dòng)力源泉柄错!
導(dǎo)航:
- 上一篇:《Objective-C 底層對(duì)象研究-上》 下一篇:待續(xù).....