本篇幅內(nèi)容較多,但是干貨滿滿,不僅涉及源碼分析還涉及模擬系統(tǒng)底層計(jì)算分配流程,建議分次食用,耐心看完相信會(huì)有很多收獲~
開發(fā)中使用最多的就是NSObject對(duì)象了,最近深入研究了一番,整理出來比較重要也是自己研究的比深入的幾個(gè)點(diǎn),通過源碼的角度來分析一下,包括對(duì)象的底層實(shí)現(xiàn),以及系統(tǒng)是如何使用內(nèi)存對(duì)齊機(jī)制來計(jì)算對(duì)象大小的,包括isa指針及superclass指針等源碼級(jí)別的分析,特做記錄,以供翻閱回顧。
一 對(duì)象的本質(zhì)
OC中的對(duì)象分為三種:
實(shí)例對(duì)象(instance對(duì)象)
存儲(chǔ)實(shí)例變量的值
類對(duì)象(calss對(duì)象)
存儲(chǔ)對(duì)象的信息、變量信息、實(shí)例方法、協(xié)議
元類對(duì)象(meta-class對(duì)象)
存儲(chǔ)類方法
對(duì)象在底層是轉(zhuǎn)變?yōu)閏++的結(jié)構(gòu)體來使用的,舉個(gè)簡(jiǎn)單的例子看,比如創(chuàng)建一個(gè)Dog類,如下所示:
@interface Dog : NSObject
{
int age;
int ID;
int number;
}
@end
@implementation Dog
@end
一個(gè)很簡(jiǎn)單的Dog類,當(dāng)編譯之后,他會(huì)被編譯成如下的結(jié)構(gòu)體:
struct Dog_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int age;
int ID;
int number;
};
很明顯,這是一個(gè)結(jié)構(gòu)體類型的數(shù)據(jù),其中NSObject_IMPL類型是NSObject編譯后的結(jié)構(gòu)體,如下所示:
struct NSObject_IMPL{
Class isa;
}
明顯NSObject里面的內(nèi)容只有一個(gè)isa指針,isa指針的作用后面會(huì)分析。根據(jù)編譯后的文件的內(nèi)容來看,對(duì)象在運(yùn)行的時(shí)候確實(shí)是以結(jié)構(gòu)體的形式存在的。編譯后的Dog結(jié)構(gòu)體里有一個(gè)編譯后的NSObject類型的結(jié)構(gòu)體數(shù)據(jù),因?yàn)樗抢^承于NSObject對(duì)象的,如果繼承于其他對(duì)象的話也會(huì)有一個(gè)其他對(duì)象的結(jié)構(gòu)體數(shù)據(jù)在里面。
順便提一下,第一部分和第二部分都會(huì)用這個(gè)簡(jiǎn)單的Dog類來分析。
二 內(nèi)存大小計(jì)算
還是以上面的Dog類來分析,看一下他在系統(tǒng)中占用的內(nèi)存大小是多少,先自己計(jì)算一下(以64位系統(tǒng)分析):
Dog類編譯后的結(jié)構(gòu)體包含一個(gè)NSObject_IMPL類型的結(jié)構(gòu)體數(shù)據(jù),這個(gè)類型的結(jié)構(gòu)體里有一個(gè)Class類型的變量,占用8個(gè)字節(jié),所以Dog類里的NSObject_IMPL變量占用8個(gè)字節(jié);
int類型的age變量占用4個(gè)字節(jié);
int類型的ID變量占用4個(gè)字節(jié);
int類型的number變量占用4個(gè)字節(jié);
綜上所屬,Dog類型的數(shù)據(jù)應(yīng)該占用8 + 4 + 4 + 4 = 20個(gè)字節(jié)。
看下系統(tǒng)的輸出計(jì)算:

可以看到系統(tǒng)給出的類的分配的空間大小為24字節(jié)(class_getInstanceSize方法),當(dāng)實(shí)際使用的時(shí)候,給實(shí)例對(duì)象分配的大小達(dá)到了32字節(jié)(malloc_size方法),這個(gè)是為什么呢?
我們從源碼的角度來對(duì)計(jì)算的方法一個(gè)個(gè)分析。
class_getInstanceSize
先看class_getInstanceSize方法。這個(gè)方法返回的是對(duì)類的實(shí)例變量分配的大小空間,而且是內(nèi)存對(duì)齊之后的大小,看源碼內(nèi)容(蘋果源碼獲取網(wǎng)站地址:https://opensource.apple.com/tarballs/):



上面的三個(gè)圖片是蘋果objc框架的源碼截圖,展示了具體的class_getInstanceSize方法的實(shí)現(xiàn),可以看到最終決定class_getInstanceSize大小的是word_align字節(jié)對(duì)齊方法,在這個(gè)方法里使用了字節(jié)對(duì)齊的方法來返回給類的變量實(shí)際分配的大小,我們根據(jù)方法的流程自己來算一下:
(x + WORD_MASK) & ~WORD_MASK;

根據(jù)上面的計(jì)算流程,class_getInstanceSize其實(shí)是進(jìn)行了一次8倍內(nèi)存對(duì)齊的操作,所以為什么系統(tǒng)計(jì)算的class_getInstanceSize方法返回的是24自己想必各位已經(jīng)很清楚了。
malloc_size
malloc_size方法返回的是對(duì)象的實(shí)例實(shí)際占用的內(nèi)存大小,malloc_size和class_getInstanceSize一樣也采用了內(nèi)存對(duì)齊機(jī)制,只不過他用的是16倍的內(nèi)存對(duì)齊機(jī)制,就不做具體分析了。相關(guān)源代碼如下:

值得一提的是,如果我們計(jì)算NSObject大小的話,算然它的對(duì)象實(shí)例僅有一個(gè)8個(gè)字節(jié)的isa指針,但是我們會(huì)發(fā)現(xiàn)malloc_size方法返回的是16字節(jié),原因如下:

OC的底層代碼里對(duì)給對(duì)象分配的最小內(nèi)存空間做了限制,限制最小為16字節(jié),所以NSObject對(duì)象雖然僅僅有一個(gè)ISA指針,但是系統(tǒng)仍然會(huì)在實(shí)際使用他的實(shí)例對(duì)象的時(shí)候給他分配16個(gè)字節(jié)的空間。內(nèi)存對(duì)齊機(jī)制是系統(tǒng)來決定的,這個(gè)機(jī)制提高了系統(tǒng)對(duì)內(nèi)存空間的訪問效率。
對(duì)象在內(nèi)存中的排列
我們?cè)诳匆幌聦?shí)例對(duì)象在使用的時(shí)候在內(nèi)存中是如何存儲(chǔ)的,為了方便內(nèi)存查看,創(chuàng)建的一個(gè)dog對(duì)象并對(duì)其賦值,如下所示:

根據(jù)對(duì)象的地址,我們看一下在內(nèi)存中是如何排列的,以及占用的字節(jié)大?。?/p>

每個(gè)數(shù)字代表一個(gè)字節(jié),圈起來一共32的字節(jié),正是malloc_size方法實(shí)際分配的字節(jié)數(shù)。
至此,對(duì)于對(duì)象在內(nèi)存中的字節(jié)分配計(jì)算和使用應(yīng)該已經(jīng)非常清晰了吧。
三 ISA指針作用
ISA指針的作用是找到方法調(diào)用信息存儲(chǔ)的對(duì)象,如果找到了就加以調(diào)用,我們引入一個(gè)SubDog的類來進(jìn)行分析,此時(shí)我們有的類如下:

我們此時(shí)有兩個(gè)類,一個(gè)繼承于NSObject的Dog類,一個(gè)繼承于Dog類的SubDog類,SubDog類里有一個(gè)實(shí)例方法和類方法,我們以SubDog類來分析一下他的實(shí)例對(duì)象,類對(duì)象,元類對(duì)象各自存儲(chǔ)的信息是什么:

上面的圖不僅僅列出來了subDog類的實(shí)例對(duì)象、類對(duì)象、元類對(duì)象的存儲(chǔ)信息,還標(biāo)明了Isa指針及superclass指針各自指向的地方。
再重新說明一下實(shí)例對(duì)象、類對(duì)象、元類對(duì)象內(nèi)存儲(chǔ)的信息:
實(shí)例對(duì)象(instance對(duì)象)
存儲(chǔ)實(shí)例變量的值
類對(duì)象(calss對(duì)象)
存儲(chǔ)對(duì)象的信息、變量信息、實(shí)例方法、協(xié)議
元類對(duì)象(meta-class對(duì)象)
存儲(chǔ)類方法
我們看一個(gè)簡(jiǎn)單的實(shí)例,如果我們調(diào)用了
[subDog testInstanceFun];
這個(gè)方法,我們對(duì)這個(gè)方法的調(diào)用進(jìn)行一個(gè)簡(jiǎn)單的分析:
1.從面向?qū)ο蟮慕嵌葋矸治?,我們?chuàng)建了一個(gè)subDog實(shí)例對(duì)象,這個(gè)對(duì)象實(shí)現(xiàn)了testInstanceFun實(shí)例方法,所以我們調(diào)用testInstanceFun是沒有問題的
2.從ISA指向的角度來分析:當(dāng)我們調(diào)用subDog的testInstanceFun實(shí)例方法的時(shí)候,實(shí)際上是先通過subDog對(duì)象的isa指針尋找到SubDog類對(duì)象,SubDog類對(duì)象里包含了testInstanceFun方法的信息,所以會(huì)直接調(diào)用testInstanceFun方法。
這個(gè)就是ISA指針在方法調(diào)用里的作用。
ISA指針深入分析
實(shí)際上ISA指針里存儲(chǔ)的就是指向?qū)ο蟮膬?nèi)存地址,我們驗(yàn)證一下實(shí)例對(duì)象的ISA指針是否是指向其類對(duì)象的:
創(chuàng)建一個(gè)SubDog的類對(duì)象和SubDog的實(shí)例對(duì)象,并打印輸出他們的地址和ISA指針(因?yàn)闊o法直接打印對(duì)象的ISA指針,所以我做了一個(gè)特殊處理然后將實(shí)例對(duì)象的ISA指針打印了出來):

可以看到 :
SubDog類對(duì)象地址是 0x00000001000022c0
SubDog實(shí)例對(duì)象地址是 0x001d8001000022c1
他倆的值并不一樣,為什么呢?
因?yàn)樵趇os的系統(tǒng)中如果需要通過ISA的地址進(jìn)行查找的話,需要使用ISA_MASK進(jìn)行&的操作之后才可以得到真正的地址,

我們?cè)囈幌拢?/p>

可以看到SubDog的實(shí)例對(duì)象通過進(jìn)行和ISA_MASK的&操作之后得到的就是SubDog類對(duì)象地址 0x00000001000022c0
因此,我們可以得知實(shí)例對(duì)象的ISA指針確實(shí)是指向其類對(duì)象的,類對(duì)象的ISA指向元類對(duì)象也是一樣的。
擴(kuò)展:
object_getClass 也是根據(jù)ISA指針來返回?cái)?shù)據(jù)的:
如果傳入的是一個(gè)實(shí)例對(duì)象,那么就會(huì)返回類對(duì)象;
如果傳入的是類對(duì)象,那么返回的就是元類對(duì)象;
如果傳入的是元類對(duì)象,那么返回的就是元類對(duì)象的基類對(duì)象。
看源碼表述的也很清晰:

objc_getClass 則是根據(jù)傳入的字符串作為key去一個(gè)map表里查找,看是否有跟傳入的key一致的類并將其返回,如果沒有匹配的話則返回空。
objc_getClass的源碼里調(diào)用的關(guān)鍵方法是:

四 superclass的作用
superclass從面向?qū)ο蟮慕嵌葋砜吹脑挘拍詈驮硎潜容^清晰的:調(diào)用方法時(shí)會(huì)從自己的方法實(shí)現(xiàn)里去找,如果沒有實(shí)現(xiàn)則去父類里去尋找,如果父類一層層也沒實(shí)現(xiàn)的話會(huì)拋出一個(gè)unrecognized異常,如果把這個(gè)流程放在我們的圖里看的話就是這樣的:

從實(shí)例對(duì)象開始->尋找實(shí)例對(duì)象的類對(duì)象看是否有方法信息->根據(jù)superclass尋找父類類對(duì)象信息->根據(jù)superclass找到NSObjct類對(duì)象->nil
還是驗(yàn)證一下SubDog的superclass指向的是否是Dog類:

很明顯SubDog的類對(duì)象superclass的值和其父類Dog類對(duì)象的地址是一致的。
再看一個(gè)比較有意思的例子:
創(chuàng)建一個(gè)NSObject的category,并且給他添加一個(gè)實(shí)例方法,如下:

將這個(gè)category引入到我們的測(cè)試文件后,然后我們直接調(diào)用:

這個(gè)時(shí)候會(huì)發(fā)生什么呢?
直接看結(jié)果:

一個(gè)類對(duì)象,居然直接調(diào)用成功了父類對(duì)象的實(shí)例方法,如果從面向?qū)ο蟮慕嵌葋砜吹脑捠呛茈y說通的吧?那我們通過isa指針和supperclass指針結(jié)合來看一下就很清晰了,如下圖:

文字說明一下整個(gè)流程:
SubDog類對(duì)象通過ISA尋找SubDog元類對(duì)象看是否有方法實(shí)現(xiàn)->SubDog元類對(duì)象通過supperclass指針尋找Dog元類對(duì)象是否有方法實(shí)現(xiàn)->Dog元類對(duì)象通過supperclass尋找到NSObject元類對(duì)象->(特殊)NSObject元類對(duì)象的supperclass是NSObject的類對(duì)象,NSObject元類對(duì)象通過supperclass找到NSObject的類對(duì)象,NSObject的類對(duì)象里記錄了我們添加的實(shí)例方法testNSObjectInstanceFun的實(shí)現(xiàn)信息,所以就可以直接調(diào)用成功了。
其實(shí)我們都知道,方法的調(diào)用是通過Objc 發(fā)送消息的機(jī)制來進(jìn)行消息傳遞然后調(diào)用的。實(shí)際上在消息發(fā)送的時(shí)候并沒有標(biāo)記這個(gè)消息方法是+號(hào)消息還是-號(hào)消息(類方法或者實(shí)例方法),不管是類方法還是實(shí)例方法都是通過方法名去找,所以我們通過類對(duì)象直接調(diào)用實(shí)例方法是可以實(shí)現(xiàn)的。
附一張網(wǎng)絡(luò)圖:

小結(jié):
至此我們的內(nèi)容介紹就全部結(jié)束了,有問題歡迎留言,我們一起探討~