Objective-C 底層對(duì)象探究-中

目錄


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)分析),這又是什么原因呢粪摘?
      index.gif
    • 我們這里不如大膽猜測(cè)一下瀑晒,OC里面的方法調(diào)用都離不開兩個(gè)東西SELIMP绍坝,SEL就是方法標(biāo)示,IMP就是指向方法具體實(shí)現(xiàn)的指針苔悦,就好比一本書的目錄一樣轩褐,你需要先查到目錄的條目之后再根據(jù)對(duì)應(yīng)的頁碼找到具體內(nèi)容。OC是動(dòng)態(tài)語言SELIMP是可以進(jìn)行動(dòng)態(tài)改變的间坐,所以alloc是存在被改變可能性的灾挨。
  • 探索調(diào)用 objc_alloc 的原因:

    • 經(jīng)過我們的分析我們已經(jīng)有了大致思路,那么我們就用過研究源碼來驗(yàn)證我們的分析是否正確竹宋。
    • 首先我們先通過搜索objc_alloc看看是否有結(jié)果......
      圖片.png
    • 哈哈劳澄!發(fā)現(xiàn)搜索出來的內(nèi)容還是挺多的,但是不要怕,經(jīng)過我的逐一排查我定位到了這里(紅框處)蜈七。
    • 從這段代碼我們就很明顯的發(fā)現(xiàn)了在runtimeallocIMP的的確確是被替換了秒拔,這個(gè)已經(jīng)證明我們分析的思路是正確的;
      圖片.png
    • 那么我們繼續(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)用的,所以果斷試了一下可以斷住会前。
      圖片.png

      圖片.png
    • 通過符號(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)單的理解)
      圖片.png
    • 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)行修改吧搔驼!~)。
  • 在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)用的。
      圖片.png
    • 再搜索tryGenerateSpecializedMessageSend函數(shù)查看調(diào)用邏輯揽思,搜索后我們來到了GeneratePossiblySpecializedMessageSend函數(shù)袜腥。
      圖片.png
    • 從代碼我們可以簡(jiǎn)要的看出,當(dāng)發(fā)送消息時(shí)會(huì)先判斷是否符合發(fā)送特殊消息的條件绰更,如果符合就嘗試通過特殊方式發(fā)送瞧挤,如果不滿足就按正常流程發(fā)送消息。按照這個(gè)邏輯就能得出一個(gè)結(jié)論了:
  • 小結(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次的真正原因了。
    圖片.png

    圖片.png
    • 接下來我們可以在深入一點(diǎn)譬淳,查看一下底層是如何處理函數(shù)調(diào)用的档址,我們可以通過tryGenerateSpecializedMessageSend函數(shù)中對(duì)alloc方法處理為例子,一步一步跟蹤邻梆,最終我們走到了下面圖片所示的位置守伸;通過上下傳參最終會(huì)通過Builder.CreateCall()Builder.CreateInvoke()進(jìn)行函數(shù)的指令調(diào)用;
      圖片.png

      圖片.png
    • 通過對(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é)束蠢涝。

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)行后顯示了占用大小。
      圖片.png
  • 發(fā)現(xiàn)影響大小的因素:

    • 增加屬性和成員變量:我們添加或者刪除一下屬性和成員變量可以觀察到场晶,對(duì)象的大小會(huì)有不同的不變化混埠,增加時(shí)大小會(huì)增大,反之亦然诗轻;


      圖片.png
    • 添加方法:屬性和變量會(huì)影響大小改變钳宪,我們也可以試試添加方法是否也會(huì)改變大小扳炬?答案是并不會(huì)吏颖。


      圖片.png
    • 到此我們可以得到一個(gè)結(jié)論:對(duì)象的內(nèi)存大小是由成員變量決定的,跟其他內(nèi)容沒有關(guān)系恨樟。
  • class_getInstanceSize()方法:

    • 我們進(jìn)入到objc源碼Command+shift+O搜索class_getInstanceSize直接就可以定位到半醉。
      圖片.png
    • 我們一步一步定位到這里給出了明確提示:May be unaligned depending on class's ivars.
      圖片.png
  • 沒有變量時(shí)打印為什么是8?:

    • 當(dāng)我們將所有定義的成員變量刪除之后劝术,通過class_getInstanceSize()方法打印結(jié)果是8缩多,這也就說明我們一定從父類中繼承過來了成員變量呆奕,我們?cè)偻ㄟ^源碼進(jìn)行驗(yàn)證。
      圖片.png
    • 我們直接搜索父類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é)蛔琅。
      圖片.png

      圖片.png
    • 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)體類型。
      圖片.png

      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é)束。

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é)尾抑。

    圖片.png

  • alignedInstanceSize()方法中我看可以看到底層系統(tǒng)將對(duì)象占用的內(nèi)存大小進(jìn)行了字節(jié)對(duì)齊歇父,我看通過word_align()了解具體對(duì)齊算法。

    圖片.png

  • 算法解析:

    • 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í)間”稍坯。
      圖片.png
  • 思路梳理:

    • 我們定義的類從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)存大小這塊是否有什么不一樣?下面就讓我們來一起探究一番滔迈。
    圖片.png
  • 結(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)體。


      圖片.png

      圖片.png
  • 測(cè)試下帶嵌套的結(jié)構(gòu)體畴椰,我新建一個(gè)ZXStruct3臊诊,然后將ZXStruct1聲明為內(nèi)部的一個(gè)成員。


    圖片.png

    圖片.png
    • 理解:
      • 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è)成員放大來觀察。
      圖片.png
    • 不采取對(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次就可以完成了讼积。
  • 至此結(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)行打印輸出;

    圖片.png

    圖片.png

  • 此時(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é)果衡未。


    圖片.png

    圖片.png
    • 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)行的)晒夹。

    圖片.png

  • 在探索源碼前我們還可以去蘋果官網(wǎng)搜索這個(gè)函數(shù)的官方解釋 malloc_size 的蘋果官網(wǎng)解釋: “返回ptr所指向的分配的內(nèi)存塊的大小裆馒。內(nèi)存塊的大小總是至少和它的分配一樣大,也可能會(huì)更大”丐怯,通過官方的解釋我們就能理解我們現(xiàn)在遇到的這個(gè)現(xiàn)象了吧喷好,現(xiàn)象就是返回的大小可能跟實(shí)際分配的一致或更大。那么接下來读跷,我們帶著這個(gè)問題來開始源碼的探索梗搅。

    圖片.png

  • 下載libmalloc可編譯的源碼:下載libmalloc可編譯的源碼

    圖片.png

  • 在上一篇文章中我們已經(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ì)象研究-上》

    圖片.png

    圖片.png

  • 我們將斷點(diǎn)斷在calloc上,來跟蹤內(nèi)存開辟的機(jī)制丐枉,編譯-運(yùn)行后我們進(jìn)入到了calloc里哆键,這只是一個(gè)封裝函數(shù),繼續(xù)跟蹤_malloc_zone_calloc()瘦锹。

    圖片.png

  • 進(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ì)有好多類似的語句,都是用于從獲取賦值的)

    圖片.png

    圖片.png

  • 我們搜索default_zone_calloc()找到位置發(fā)現(xiàn)又調(diào)用了zone這個(gè)對(duì)象中calloc的方法蝙斜,我們繼續(xù)po它得到結(jié)果名惩。

    圖片.png

    圖片.png

    圖片.png

  • 我們?cè)賹ふ?code>nano_malloc.c文件的878行,根據(jù)分析我們可以分析出return p 是正確的路線孕荠,p是通過_nano_malloc_check_clear()函數(shù)返回的娩鹉,我們繼續(xù)就探索下去攻谁。

    圖片.png

    • 進(jìn)到_nano_malloc_check_clear()我們可以將復(fù)雜的方法簡(jiǎn)單化處理下,先將不重要的判斷隱藏掉弯予。
      圖片.png
  • 思路分析:

    • *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;
    
    圖片.png

    圖片.png
  • 算法解析:

    • 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ì)齊的差別與意義栈暇;
注:
寫到最后
  • 到此本篇內(nèi)容以及結(jié)束!如果您喜歡的話別忘了賞個(gè)贊适袜!您的點(diǎn)贊是我最大的動(dòng)力源泉柄错!
導(dǎo)航:
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市痪蝇,隨后出現(xiàn)的幾起案子鄙陡,更是在濱河造成了極大的恐慌冕房,老刑警劉巖躏啰,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異耙册,居然都是意外死亡给僵,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來帝际,“玉大人蔓同,你說我怎么就攤上這事《拙鳎” “怎么了斑粱?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)脯爪。 經(jīng)常有香客問我则北,道長(zhǎng),這世上最難降的妖魔是什么痕慢? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任尚揣,我火速辦了婚禮,結(jié)果婚禮上掖举,老公的妹妹穿的比我還像新娘快骗。我一直安慰自己,他們只是感情好塔次,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布方篮。 她就那樣靜靜地躺著,像睡著了一般励负。 火紅的嫁衣襯著肌膚如雪恭取。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天熄守,我揣著相機(jī)與錄音蜈垮,去河邊找鬼。 笑死裕照,一個(gè)胖子當(dāng)著我的面吹牛攒发,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播晋南,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼惠猿,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了负间?” 一聲冷哼從身側(cè)響起偶妖,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎政溃,沒想到半個(gè)月后趾访,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡董虱,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年扼鞋,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了申鱼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡云头,死狀恐怖捐友,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情溃槐,我是刑警寧澤匣砖,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站昏滴,受9級(jí)特大地震影響脆粥,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜影涉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一变隔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蟹倾,春花似錦匣缘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至豁陆,卻和暖如春柑爸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背盒音。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國打工表鳍, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人祥诽。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓譬圣,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親雄坪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子厘熟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容