第二節(jié)課 OC對(duì)象原理(中)
底層LLVM優(yōu)化
上篇文章我們說到,實(shí)際代碼查看的流程是 alloc->_objc_rootAlloc->callAlloc->_objc_rootAllocWithZone,但是,我們通過斷點(diǎn)發(fā)現(xiàn)實(shí)際流程卻是alloc->objc_alloc->callAlloc->objc_msgSend->alloc->_objc_rootAlloc-callAlloc,這個(gè)到底是為啥呢?
原因是蘋果覺得alloc是比較特殊的方法,只要是alloc,就先走objc_alloc(類似hook),執(zhí)行了一些底層優(yōu)化、標(biāo)記,再執(zhí)行alloc方法,具體需要探索LLVM的源碼,這個(gè)我們后續(xù)再進(jìn)行補(bǔ)充。
LLVM下載地址
所以實(shí)際的alloc流程應(yīng)該為:
alloc->objc_alloc->LLVM底層優(yōu)化、標(biāo)記等等->objc_alloc->objc_msgSend->alloc->LLVM判斷標(biāo)記過->_objc_rootAlloc
這也是為什么callalloc走兩次的原因,所以我們將之前的流程圖再次補(bǔ)充一下。

對(duì)象的內(nèi)存的影響因素
上篇文章我們討論了一下字節(jié)對(duì)齊,字節(jié)對(duì)齊的最終又是以內(nèi)存的方式展現(xiàn),所以我們來探究一下能影響內(nèi)存的因素。
先看下我們上篇文章寫的例子

正常的class_getInstanceSize,是32,那我們干掉一些屬性后呢?發(fā)現(xiàn)有減少

這證明我們的屬性是對(duì)內(nèi)存有影響的,那成員變量應(yīng)該也是一樣的,我們添加后發(fā)現(xiàn),確實(shí)影響了內(nèi)存大小。


那么方法呢?添加了一個(gè)方法后發(fā)現(xiàn)并沒有變化,因?yàn)榉椒ú徽加脙?nèi)存,這個(gè)我們后續(xù)會(huì)再詳細(xì)進(jìn)行講解。對(duì)于內(nèi)存的理解我們可以先看下面的圖。

Person通過alloc開辟了一塊堆的空間,外部通過對(duì)象的地址(棧里)來進(jìn)行指向。這個(gè)內(nèi)存空間里的就是Person里面的各項(xiàng)成員變量以及isa。

我們新增幾條屬性后,通過x/8gx輸出看到,左邊0x60000336b6c0為首地址,對(duì)應(yīng)的第一個(gè)對(duì)象是0x0000000101968888,也就是isa。后面按順序排列的依次是各個(gè)屬性變量,每8字節(jié)一個(gè)對(duì)象,這也就是我們對(duì)齊原則。
需要注意的一點(diǎn)是我們的190.5是po不出來,我們可以使用e -f f-- 0x4067d00000000000,或者p/f 0x4067d00000000000.因?yàn)槲覀冋5膒o打印不出來,double與float類型需要單獨(dú)輸出打印
結(jié)構(gòu)體內(nèi)存對(duì)齊
剛才的例子中,我們發(fā)現(xiàn)第二個(gè)變量,0x0000001200006261實(shí)際上是3個(gè)變量組成的,0x12、0x62、0x61,這個(gè)是由于進(jìn)行了響應(yīng)的內(nèi)存對(duì)齊,那我們就來看看結(jié)構(gòu)體的內(nèi)存對(duì)齊原則
1:數(shù)據(jù)成員對(duì)齊規(guī)則:結(jié)構(gòu)(struct)(或聯(lián)合(union))的數(shù)據(jù)成員,第一個(gè)數(shù)據(jù)成員放在offset為0的地方,以后每個(gè)數(shù)據(jù)成員存儲(chǔ)的起始位置要從該成員大小或者成員的子成員大小(只要該成員有子成員,比如說是數(shù)組,結(jié)構(gòu)體等)的整數(shù)倍開始(比如int在32位機(jī)為4字節(jié),則要從4的整數(shù)倍地址開始存儲(chǔ)。
2:結(jié)構(gòu)體作為成員:如果一個(gè)結(jié)構(gòu)里有某些結(jié)構(gòu)體成員,則結(jié)構(gòu)體成員要從其內(nèi)部最大元素大小的整數(shù)倍地址開始存儲(chǔ).(struct a里存有struct b,b里有char,int ,double等元素,那b應(yīng)該從8的整數(shù)倍開始存儲(chǔ).)
3:收尾工作:結(jié)構(gòu)體的總大小,也就是sizeof的結(jié)果,.必須是其內(nèi)部最大成員的整數(shù)倍.不足的要補(bǔ)齊。
我們先來看兩個(gè)結(jié)構(gòu)體
struct HZMStruct1 {
double a;
char b;
int c;
short d;
}struct1;
struct HZMStruct2 {
double a;
int b;
char c;
short d;
}struct2;
這兩個(gè)結(jié)構(gòu)體的成員變量完全一樣,只是順序不一樣,這樣的兩個(gè)結(jié)構(gòu)體的sizeof會(huì)一樣嘛?直接上結(jié)果

Why?
double a; // 8字節(jié) 存儲(chǔ)位置[0 7]
char b; // 1字節(jié) [8]
int c; // 4字節(jié) (9 10 11 [12 13 14 15]不是整數(shù)倍數(shù)的位置pass掉
short d; // 2字節(jié) [16 17] 24
}struct1;
struct HZMStruct2 {
double a; // 8字節(jié) [0 7]
int b; // 4字節(jié) [8 9 10 11]
char c; // 1字節(jié) [12]
short d; // 2字節(jié) (13 [14 15] 16
}struct2;
在我們存儲(chǔ)的過程中,其實(shí)蘋果會(huì)自動(dòng)幫我們進(jìn)行最優(yōu)化排序
在上面的過程中,我們將不是當(dāng)前對(duì)象的整數(shù)倍的存儲(chǔ)位置pass掉了,這是為什么?我們通過畫圖來理解

當(dāng)我們看到第一種取法,發(fā)現(xiàn)每次變化取值長(zhǎng)度,一共需要3次才取完,而第二種取法直接按照最大長(zhǎng)度去取,不夠的位置空出來,后續(xù)如果有滿足條件的在優(yōu)化的過程中插進(jìn)去(1+4+3->1+3+4)這樣我們只需要兩次就全部取完了,這與我們第一篇文章講的字節(jié)對(duì)齊,以空間換時(shí)間,是異曲同工之妙。
下面我們?cè)僮鲆粋€(gè)練習(xí)鞏固下
struct LGStruct3 {
double a; // 8字節(jié) [0 7]
int b; // 4字節(jié) [8 9 10 11]
char c; // 1字節(jié) [12]
short d; // 2字節(jié) (13 [14 15]
int e; // 4字節(jié) [16 17 18 19] 24
struct HZMStruct1 str; (20 21 22 23 [24~ 41] ->48
}struct3;
malloc源碼引入


第一個(gè):<LGPerson: 0x100542960>應(yīng)該不用過多解釋了
第二個(gè):我們可以理解為person是個(gè)對(duì)象,對(duì)象的本質(zhì)就是指針地址,指針大小為8字節(jié)
第三個(gè):LGPerson的各個(gè)成員變量相加,8+8+4+8=28 ->32 但是要注意還有一個(gè)isa,所以32+8=40
第四個(gè):40->48是因?yàn)樯赌??我們看?code>malloc_size我們只能通過源碼進(jìn)行分析
接下來就就還是進(jìn)入我們的源碼文件進(jìn)行分析
malloc分析探索思路
首先從alloc進(jìn)入objc的源碼,找到obj = (id)calloc(1, size);
操作,涉及的方法順序是alloc --> _objc_rootAlloc --> callAlloc --> _objc_rootAllocWithZone --> _class_createInstanceFromZone

這里calloc的探索需要切換到 libmalloc源碼中,可以在opensource下載最新版,接著往下走
1、在可編譯的libmalloc中定義一個(gè)可編譯的target,在main中使用calloc創(chuàng)建一個(gè)指針

2、進(jìn)入_malloc_zone_calloc的源碼實(shí)現(xiàn),關(guān)鍵代碼是1560行的zone->calloc(zone, num_items, size);

3、進(jìn)入zone->alloc的源碼,源碼就無法繼續(xù)跟進(jìn)了

重點(diǎn):為了繼續(xù)深入了解,我們?cè)?code>ptr = zone->calloc(zone, num_items, size);處,加一個(gè)斷點(diǎn),然后運(yùn)行。
斷住后,通過打印得知zone->calloc的源碼實(shí)現(xiàn)在default_zone_calloc方法,然后全局搜索default_zone_calloc方法,找到具體實(shí)現(xiàn)

4、進(jìn)入calloc的源碼實(shí)現(xiàn),其中主要由兩部分操作
- 創(chuàng)建真正的
zone,即runtime_default_zone方法 - 使用真正的
zone進(jìn)行calloc
02-default_zone_calloc.png
5、斷點(diǎn)走到return后,繼續(xù)打印

6、搜索nano_calloc進(jìn)入,其中的關(guān)鍵代碼是888行的返回值,此時(shí)的p是pointer表示指針 和前面的 ptr一樣

7、進(jìn)入_nano_malloc_check_clear源碼,將if else 折疊,看主流程

其中
segregated_next_block就是指針內(nèi)存開辟算法,目的是找到合適的內(nèi)存并返回slot_bytes是加密算法的鹽(其目的是為了讓加密算法更加安全,本質(zhì)就是一串自定義的數(shù)字)
8、進(jìn)入segregated_next_block方法,這個(gè)方法主要就是獲取內(nèi)存指針

整個(gè)流程大概意思就是不斷循環(huán)查找能夠容納需要的大小的空間,如果找到直接返回空間地址,如果找不到返回0。
9、進(jìn)入segregated_size_to_fit加密算法源碼, 通過算法邏輯,可以看出,其本質(zhì)就會(huì)16字節(jié)對(duì)齊算法

所以在我們的堆里面,整個(gè)對(duì)象的內(nèi)存是以16字節(jié)對(duì)齊,成員變量是以8字節(jié)對(duì)齊(結(jié)構(gòu)體內(nèi)部),對(duì)象與對(duì)象之間因?yàn)槭窃谡麄€(gè)內(nèi)存中,所以也是16字節(jié)對(duì)齊

所以我們之前的輸出結(jié)果為 <LGPerson: 0x100542960> - 8 - 40 - 48 最后一項(xiàng)是48
