「本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!」前端
學習不迷茫,無阻我飛揚!你們好我是Tommy!今天咱們繼續來對底層進行探索,本章內容會比較多,裏面的可能有些知識不太好理解,你們能夠分小節進行閱讀。廢話不說咱們這就開始!git
alloc
的運行流程進行了梳理,但這裏存在一個問題不知道你們是否發現了?就是咱們經過符號斷點等方式發現,alloc最早是調用了objc_alloc
方法後再開始走調用流程的;(動態分析)
alloc
調用的並非objc_alloc
而是_objc_rootAlloc
函數(靜態分析)
,這又是什麼緣由呢? SEL
和IMP
,SEL
就是方法標示,IMP
就是指向方法具體實現的指針,就比如一本書的目錄同樣,你須要先查到目錄的條目以後再根據對應的頁碼找到具體內容。OC是動態語言SEL
和IMP
是能夠進行動態改變的,因此alloc
是存在被改變可能性的。objc_alloc
看看是否有結果......runtime
中alloc
的IMP
的的確確是被替換了,這個已經證實咱們分析的思路是正確的;fixupMessageRef
函數是在何時被調用的?繼續經過搜索來得出答案。fixupMessageRef
函數是在_read_images
這個函數中被調用的。_read_images
方法上面的註釋:「對連接中的頭信息執行初始化處理」,應該能夠猜到_read_images
方法可能與DYLD加載Mach-O文件有必定關係。咱們能夠給map_images_nolock
下個符號斷點,爲啥呢?由於_read_images
我測試了沒法斷住,根據方法上面的註釋得知是經過map_images_nolock
這個函數調用的,因此果斷試了一下能夠斷住。alloc
確實是在runtime
的源碼中有被替換的跡象;fixupMessageRef
這個方法名稱,咱們能夠理解程序在運行時,須要對alloc
等一些方法進行修復處理;那咱們是否是能夠理解成:無論當前是否存在問題,alloc
方法始終都會被改動調用objc_alloc
;fixupMessageRef
方法是在_read_images
中被調用的,而_read_images
是在DYLD加載Mach-O文件時進行加載的;Mach-O文件中會存在一個叫作符號列表的內容,裏面就會將App的方法存放到此表中,當DYLD加載時就會讀取列表進行映射操做,而這個過程就叫作符號綁定(如今能夠先這麼簡單的理解)objc_alloc
,若是存在問題就經過fixupMessageRef
方法進行修復處理,而處理結果依然是調用objc_alloc
,這一點須要你們細品一下。 若是以上思路都明確以後,咱們應該會想到alloc方法在運行時作的只是修復工做,那麼其實真正對alloc方法進行修改的並非在運行時,實際上可能仍是在更底層進行修改的,而只是在runtime層增長了修復的邏輯,極可能是蘋果出於嚴謹性的考慮,在這一步額外增長的一層保護(多是爲了防止開發人員經過hook等方式對alloc方法進行修改吧!~)。LLVM-project
這裏是連接[LLVM-project下載],建議使用VSCode
進行打開。objc_alloc
看看有什麼結果,咱們點擊第一個結果就能發現這些線索;「當此方法返回true時,Clang將把某些選擇器的非超級消息發送轉換爲對相應入口點的調用」,經過這條註釋以及下面的alloc => objc_alloc
例子咱們就能夠明白了,在編譯階段alloc
就已經被進行了轉換設置。shouldUseRuntimeFunctionsForAlloc
函數看看調用邏輯,發現是在tryGenerateSpecializedMessageSend
函數中進行調用的。tryGenerateSpecializedMessageSend
函數查看調用邏輯,搜索後咱們來到了GeneratePossiblySpecializedMessageSend
函數。就是當alloc()第一次執行時,被LLVM按特殊消息發送來處理了,底層將目標轉換成了objc_alloc();objc_alloc執行後第一次調用了callAlloc();github
首次進入callAlloc()後去執行objc_msgSend的方法,又再一次調用了alloc(),可是此次LLVM是按正常方式進行處理,發送給了_objc_rootAlloc();_objc_rootAlloc()執行後第二次調用了callAlloc();而後開始對內存進行對象內存的開闢工做直至完成。web
alloc
流程圖,在這幅圖中咱們當時發現callAlloc()
被執行了2次,那麼咱們將咱們今天探索獲得的結果,添加到這幅流程圖中進行補完,你們能夠對比看一下就能瞭解callAlloc
爲何會被調用了2次
的真正緣由了。tryGenerateSpecializedMessageSend
函數中對alloc
方法處理爲例子,一步一步跟蹤,最終咱們走到了下面圖片所示的位置;經過上下傳參最終會經過Builder.CreateCall()
跟Builder.CreateInvoke()
進行函數的指令調用;HOOK
方式的處理,這裏猜想應該是對這些方法進行了一些監測和監控處理。到此本小節結束。查看對象佔用內存的大小:objective-c
ZXPerson
的對象在內存中佔用了多少空間,咱們能夠經過class_getInstanceSize()
方法打印大小,使用此方法時請導入 #import <objc/runtime.h>
頭文件。編譯運行後顯示了佔用大小。發現影響大小的因素:算法
class_getInstanceSize()方法:後端
Command+shift+O
搜索class_getInstanceSize
直接就能夠定位到。沒有變量時打印爲何是8?:緩存
class_getInstanceSize()
方法打印結果是8
,這也就說明咱們必定從父類中繼承過來了成員變量,咱們再經過源碼進行驗證。NSObject
,就會看到父類中存在一個變量叫作isa
;那麼第一個疑問就解開了,確實從父類中繼承了變量過來;那麼大小爲何是8
呢?咱們繼續分析。isa
的類型是Class
,咱們跟蹤一下看看有什麼結果,Command+shift+O
搜索Class
,發現Class是一個類型定義,實際是objc_class
類型的指針類型,而在arm64
下一個指針正好是佔用8
個字節。
objc_class
是一個結構體而且繼承objc_object
,那麼咱們自定義的類在底層實際都變成了objc_object
。咱們能夠經過clang命令對.m文件進行編譯。(個人實例程序都寫在了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++
文件咱們就能看到咱們定義的類在編譯以後都會變成objc_object
結構體類型。 ps:這麼作的目的是蘋果爲了在底層對開發人員定義的類進行統一處理而進行了轉換,由於蘋果不可能在底層去逐一的去實現開發人員定義的類,這是不可能定義出來的,由於可變性太大了;因此爲了方便對類進行管理和操做,就必須設計一個通用的類型來替代。
markdown
通源碼探究咱們發現Object-C
的底層都是經過C/C++
來實現的,因此OC
中的對象也會轉化成C/C++
中的某一個數據結構,到此本小結結束。數據結構
Object-C
的底層都是經過C/C++
來實現的,因此OC
中的對象也會轉化成C/C++
中的某一個數據結構。_class_createInstanceFromZone()
裏找到instanceSize()
,經過上一篇的探索咱們已經得知了,該方法是負責返回對象所需的空間大小的;咱們跟蹤進去能夠看到優先從緩存中查找大小,若是緩存沒有就從新計算大小,最後還有一個判斷就是若是計算的大小不足16字節
,就補足16字節
。 alignedInstanceSize()
方法中我看能夠看到底層系統將對象佔用的內存大小進行了字節對齊,我看經過word_align()
瞭解具體對齊算法。 WORD_MASK
的值是7UL
,其實就是7
;(UL
的意思是 unsignedLong
無符號長整型);假如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)
8字節
進行對齊,不足8就按8算,超過8就以8的倍數進行,例如9:就按8的2倍計算也就是16;若是是20就按8的3倍計算也就是24(你們能夠自行驗證)(ps:~7 是意思是非7 就是按7的二進制取反)
CPU
讀內的效率將內存統一按一個大小進行對齊處理,實際佔用的大小不足時,就經過補0
方式對齊。這麼作雖然犧牲了必定的內存空間,可是讀取的效率會大幅提高,也就是用 「空間換時間」。NSObject
裏集成了isa
屬性佔用8
字節;instanceSize()
得知對象內部結構是已8
字節進行對齊,但系統是最小給分配了16
字節;(x + WORD_MASK) & ~WORD_MASK
方式進行計算;8
字節對齊?這是由於在arm64
下,8
字節基本上就是最大的佔用字節數了。16
字節會怎麼樣?其實在最後底層還會以16
字節進行一次對齊處理,請看下一個小節內容結構體內存對齊。x/4gx
查看了類對象中在內存中的存放狀態,其中咱們發現了一個現象就是一個8字節的空間裏面存放了2個不一樣的數據,這種現象就叫作內存對齊而且作了相關優化處理。當咱們建立一個對象指針時,該指針實際指向的是一個結構體類型,那麼對於結構體來講內存大小這塊是否有什麼不同?下面就讓咱們來一塊兒探究一番。ZXStruct3
的第一個成員佔用到第 3
個字節位置,根據 原則2
應按照結構內部最大元素的大小的整數倍開始存儲,因此從 8
開始;而後再用 8 + zx_t1
大小,就能夠直接得出實際大小了也就是 8 + 24 = 32
。原則2
對齊,最後加上嵌套結構體就是最終的大小結果。ZXStruct1
前三個成員爲例,將3個成員放大來觀察。8
位讀取,p1
能夠一次讀完,再次按8
位讀取的時候就發現沒法正確讀取了,由於發現後8
位包含了混合數據,因此須要根據成員大小調整步長讀取,共須要4次
完成,這樣就會下降效率。8
位讀取,p1
能夠一次讀完,這個沒有發生改變,後面讀取時判斷含有混合數據的話,按數據中最大的佔位進行讀取,而且將補位的空位進行合併,(反正最後都須要補位,不如將空位移動到前面一塊兒讀取來提升效率)因此讀取3次
就能夠完成了。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 |
首先咱們先來看一個現象,我對ZXPerson
類的對象*zxp
分別經過class_getInstanceSize()
、sizeof()
、malloc_size()
、3
個函數進行打印輸出;
此時咱們ZXPerson類中定義了4個屬性再加上隱藏屬性isa,一共是5個屬
class_getInstanceSize()
打印了32
, 這個沒有問題(8+8+8+4+1 最後按8字節對齊 = 32)
;sizeof()
打印了8
,這個沒有問題(由於打印的是指針,指針的大小就是8佔字節)
;malloc_size()
打印了32
,跟class_getInstanceSize()
同樣,貌似也應該沒有問題;此時咱們ZXPerson類中新增一個屬性zxNikeName再來看看結果。
class_getInstanceSize()
打印了40
沒毛病!(8+8+8+4+1+8 最後按8字節對齊正好 = 40)
sizeof()
沒變化;malloc_size()
結果卻不一樣了變成了48
,奇奇怪怪的事情就這樣神奇的發生了!那麼爲何呢?接下來咱們來一塊兒探索一下。首先咱們先經過追蹤下malloc_size()
,從註釋「Returns size of given ptr」
咱們得知malloc_size()
函數會根據ptr
來返回大小值,而ptr
就是咱們傳入的指針。當咱們想繼續往下追蹤時發現已經沒法往下走了。那怎麼辦呢?首先不要慌!咱們肯定一下這個malloc_size()
函數的所在位置是在哪裏,從上面的導航咱們能夠看到這個函數是在malloc
這個庫下面。咱們就能夠再經過源碼方式來進行研究了(往後咱們探究的思路都是以這個方式來進行的)
。
在探索源碼前咱們還能夠去蘋果官網搜索這個函數的官方解釋 malloc_size
的蘋果官網解釋: 「返回ptr所指向的分配的內存塊的大小。內存塊的大小老是至少和它的分配同樣大,也可能會更大」
,經過官方的解釋咱們就能理解咱們如今遇到的這個現象了吧,現象就是返回的大小可能跟實際分配的一致或更大。那麼接下來,咱們帶着這個問題來開始源碼的探索。
下載libmalloc
可編譯的源碼:下載libmalloc可編譯的源碼
在上一篇文章中咱們已經對alloc
的開闢流程進行了梳理,發現 alloc
申請內存是 calloc
發起的,因此咱們直接把斷點斷到calloc
上。對於這塊不清楚的同窗請走傳送門 《Objective-C 底層對象研究-上》
咱們將斷點斷在calloc
上,來跟蹤內存開闢的機制,編譯-運行後咱們進入到了calloc
裏,這只是一個封裝函數,繼續跟蹤_malloc_zone_calloc()
。
進來後咱們能夠觀察一下,根據上面的官方文檔的說明,咱們只需關注ptr
就能夠了,那麼咱們就定位到了1560
行。可是在想從1560
行往下走就走不到了(不管是搜索關鍵字,符號斷點都沒法定位)
。仔細觀察後發現是經過zone
這個對象中calloc
的方法返回的,這時咱們能夠經過LLDB
命令 po zone->calloc
進行查看,返回的結果就是實際調用。 (這個zone->calloc其實能夠理解成是一個賦值語句,從這個zone->calloc中獲取到相關的函數去執行,當搜索 「=zone->calloc」關鍵字時,會有好多相似的語句,都是用於從獲取賦值的)
咱們搜索default_zone_calloc()
找到位置發現又調用了zone
這個對象中calloc
的方法,咱們繼續po
它獲得結果。
咱們再尋找nano_malloc.c
文件的878
行,根據分析咱們能夠分析出return p
是正確的路線,p
是經過_nano_malloc_check_clear()
函數返回的,咱們繼續就探索下去。
_nano_malloc_check_clear()
咱們能夠將複雜的方法簡單化處理下,先將不重要的判斷隱藏掉。思路分析:
*ptr
從堆區開闢空間,若是ptr
沒有,就循環進行查找。segregated_next_block()
函數你們能夠本身看一下,內部是一個while
死循環,我這裏不作過多介紹;(額……這裏仍是囉嗦一下吧,這個函數的功能就是在堆區不斷的進行查找,找到合適的位置就分配存儲地址,由於堆存儲是否是按序的,數據之間存在不規則的空隙,因此須要不斷的循環來進行處理)
*ptr
是新開闢的,因此最終仍是會走到segregated_next_block()
這步,並將上面算好的slot_bytes
大小傳遞過來進行開闢工做。segregated_size_to_fit()
函數進行處理的了,咱們能夠追蹤進去。追蹤到segregated_size_to_fit()
後咱們就看到了NANO_REGIME_QUANTA_SIZE
宏定義,追蹤進去查看發現是讓1左移了4位也就是16,最後再經過公式來進行對齊運算。
//16字節對齊公式:
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)
slot_bytes = (size + NANO_REGIME_QUANTA_SIZE - 1) & ~ SHIFT_NANO_QUANTUM
到此就知道了用malloc_size()
打印對象是48
的緣由了,由於進行了16
字節對齊。
8
字節對齊;16
字節對齊;8
字節對齊?
NSObject
,因此每一個類默認都會包含一個8
字節的isa
屬性,若是隨便增長1
個變量就已經超過8
字節(也就是最少也是16
字節起步),因此蘋果索性就按16
字節進行對齊處理下降運算次數。C語言
、C++
、Objective-C
語言的輕量級編譯器。源代碼發佈於BSD
協議下。Clang
將支持其普通lambda
表達式、返回類型的簡化處理以及更好的處理constexpr
關鍵字。LLVM
是構架編譯器(compiler)的框架系統,以C++
編寫而成,用於優化以任意程序語言編寫的程序的編譯時間(compile-time)、連接時間(link-time)、運行時間(run-time)以及空閒時間(idle-time),對開發者保持開放,併兼容已有腳本。