iOS NSObject對象的本質(zhì)、內(nèi)存分配、ISA指針及superclass底層源碼分析

本篇幅內(nèi)容較多,但是干貨滿滿,不僅涉及源碼分析還涉及模擬系統(tǒng)底層計算分配流程,建議分次食用,耐心看完相信會有很多收獲~

開發(fā)中使用最多的就是NSObject對象了,最近深入研究了一番,整理出來比較重要也是自己研究的比深入的幾個點,通過源碼的角度來分析一下,包括對象的底層實現(xiàn),以及系統(tǒng)是如何使用內(nèi)存對齊機(jī)制來計算對象大小的,包括isa指針及superclass指針等源碼級別的分析,特做記錄,以供翻閱回顧。

一 對象的本質(zhì)

OC中的對象分為三種:

實例對象(instance對象)
存儲實例變量的值

類對象(calss對象)
存儲對象的信息、變量信息、實例方法、協(xié)議

元類對象(meta-class對象)
存儲類方法

對象在底層是轉(zhuǎn)變?yōu)閏++的結(jié)構(gòu)體來使用的,舉個簡單的例子看,比如創(chuàng)建一個Dog類,如下所示:

@interface Dog : NSObject
{
 int age;
 int ID;
 int number;
}
@end

@implementation Dog
@end

一個很簡單的Dog類,當(dāng)編譯之后,他會被編譯成如下的結(jié)構(gòu)體:

struct Dog_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int age;
int ID;
int number;
};

很明顯,這是一個結(jié)構(gòu)體類型的數(shù)據(jù),其中NSObject_IMPL類型是NSObject編譯后的結(jié)構(gòu)體,如下所示:

struct NSObject_IMPL{
Class isa;
}

明顯NSObject里面的內(nèi)容只有一個isa指針,isa指針的作用后面會分析。根據(jù)編譯后的文件的內(nèi)容來看,對象在運行的時候確實是以結(jié)構(gòu)體的形式存在的。編譯后的Dog結(jié)構(gòu)體里有一個編譯后的NSObject類型的結(jié)構(gòu)體數(shù)據(jù),因為它是繼承于NSObject對象的,如果繼承于其他對象的話也會有一個其他對象的結(jié)構(gòu)體數(shù)據(jù)在里面。

順便提一下,第一部分和第二部分都會用這個簡單的Dog類來分析。

二 內(nèi)存大小計算

還是以上面的Dog類來分析,看一下他在系統(tǒng)中占用的內(nèi)存大小是多少,先自己計算一下(以64位系統(tǒng)分析):

Dog類編譯后的結(jié)構(gòu)體包含一個NSObject_IMPL類型的結(jié)構(gòu)體數(shù)據(jù),這個類型的結(jié)構(gòu)體里有一個Class類型的變量,占用8個字節(jié),所以Dog類里的NSObject_IMPL變量占用8個字節(jié);
int類型的age變量占用4個字節(jié);
int類型的ID變量占用4個字節(jié);
int類型的number變量占用4個字節(jié);
綜上所屬,Dog類型的數(shù)據(jù)應(yīng)該占用8 + 4 + 4 + 4 = 20個字節(jié)。

看下系統(tǒng)的輸出計算:

Dog類系統(tǒng)計算結(jié)果

可以看到系統(tǒng)給出的類的分配的空間大小為24字節(jié)(class_getInstanceSize方法),當(dāng)實際使用的時候,給實例對象分配的大小達(dá)到了32字節(jié)(malloc_size方法),這個是為什么呢?

我們從源碼的角度來對計算的方法一個個分析。

class_getInstanceSize
先看class_getInstanceSize方法。這個方法返回的是對類的實例變量分配的大小空間,而且是內(nèi)存對齊之后的大小,看源碼內(nèi)容(蘋果源碼獲取網(wǎng)站地址:https://opensource.apple.com/tarballs/):

class_getInstanceSize 源碼

class_getInstanceSize

class_getInstanceSize源碼

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

內(nèi)存分配計算

根據(jù)上面的計算流程,class_getInstanceSize其實是進(jìn)行了一次8倍內(nèi)存對齊的操作,所以為什么系統(tǒng)計算的class_getInstanceSize方法返回的是24自己想必各位已經(jīng)很清楚了。

malloc_size
malloc_size方法返回的是對象的實例實際占用的內(nèi)存大小,malloc_size和class_getInstanceSize一樣也采用了內(nèi)存對齊機(jī)制,只不過他用的是16倍的內(nèi)存對齊機(jī)制,就不做具體分析了。相關(guān)源代碼如下:

malloc_size

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

malloc_size源碼

OC的底層代碼里對給對象分配的最小內(nèi)存空間做了限制,限制最小為16字節(jié),所以NSObject對象雖然僅僅有一個ISA指針,但是系統(tǒng)仍然會在實際使用他的實例對象的時候給他分配16個字節(jié)的空間。內(nèi)存對齊機(jī)制是系統(tǒng)來決定的,這個機(jī)制提高了系統(tǒng)對內(nèi)存空間的訪問效率。

對象在內(nèi)存中的排列

我們在看一下實例對象在使用的時候在內(nèi)存中是如何存儲的,為了方便內(nèi)存查看,創(chuàng)建的一個dog對象并對其賦值,如下所示:


內(nèi)存排列

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

內(nèi)存排列

每個數(shù)字代表一個字節(jié),圈起來一共32的字節(jié),正是malloc_size方法實際分配的字節(jié)數(shù)。

至此,對于對象在內(nèi)存中的字節(jié)分配計算和使用應(yīng)該已經(jīng)非常清晰了吧。

三 ISA指針作用

ISA指針的作用是找到方法調(diào)用信息存儲的對象,如果找到了就加以調(diào)用,我們引入一個SubDog的類來進(jìn)行分析,此時我們有的類如下:

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

上面的圖不僅僅列出來了subDog類的實例對象、類對象、元類對象的存儲信息,還標(biāo)明了Isa指針及superclass指針各自指向的地方。

再重新說明一下實例對象、類對象、元類對象內(nèi)存儲的信息:
實例對象(instance對象)
存儲實例變量的值

類對象(calss對象)
存儲對象的信息、變量信息、實例方法、協(xié)議

元類對象(meta-class對象)
存儲類方法

我們看一個簡單的實例,如果我們調(diào)用了

[subDog testInstanceFun];

這個方法,我們對這個方法的調(diào)用進(jìn)行一個簡單的分析:

1.從面向?qū)ο蟮慕嵌葋矸治?,我們?chuàng)建了一個subDog實例對象,這個對象實現(xiàn)了testInstanceFun實例方法,所以我們調(diào)用testInstanceFun是沒有問題的

2.從ISA指向的角度來分析:當(dāng)我們調(diào)用subDog的testInstanceFun實例方法的時候,實際上是先通過subDog對象的isa指針尋找到SubDog類對象,SubDog類對象里包含了testInstanceFun方法的信息,所以會直接調(diào)用testInstanceFun方法。

這個就是ISA指針在方法調(diào)用里的作用。

ISA指針深入分析

實際上ISA指針里存儲的就是指向?qū)ο蟮膬?nèi)存地址,我們驗證一下實例對象的ISA指針是否是指向其類對象的:

創(chuàng)建一個SubDog的類對象和SubDog的實例對象,并打印輸出他們的地址和ISA指針(因為無法直接打印對象的ISA指針,所以我做了一個特殊處理然后將實例對象的ISA指針打印了出來):

可以看到 :
SubDog類對象地址是 0x00000001000022c0
SubDog實例對象地址是 0x001d8001000022c1

他倆的值并不一樣,為什么呢?
因為在ios的系統(tǒng)中如果需要通過ISA的地址進(jìn)行查找的話,需要使用ISA_MASK進(jìn)行&的操作之后才可以得到真正的地址,

我們試一下:

可以看到SubDog的實例對象通過進(jìn)行和ISA_MASK的&操作之后得到的就是SubDog類對象地址 0x00000001000022c0

因此,我們可以得知實例對象的ISA指針確實是指向其類對象的,類對象的ISA指向元類對象也是一樣的。

擴(kuò)展:
object_getClass 也是根據(jù)ISA指針來返回數(shù)據(jù)的:
如果傳入的是一個實例對象,那么就會返回類對象;
如果傳入的是類對象,那么返回的就是元類對象;
如果傳入的是元類對象,那么返回的就是元類對象的基類對象。
看源碼表述的也很清晰:


object_getClass

objc_getClass 則是根據(jù)傳入的字符串作為key去一個map表里查找,看是否有跟傳入的key一致的類并將其返回,如果沒有匹配的話則返回空。

objc_getClass的源碼里調(diào)用的關(guān)鍵方法是:

四 superclass的作用

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

superclass

從實例對象開始->尋找實例對象的類對象看是否有方法信息->根據(jù)superclass尋找父類類對象信息->根據(jù)superclass找到NSObjct類對象->nil

還是驗證一下SubDog的superclass指向的是否是Dog類:


很明顯SubDog的類對象superclass的值和其父類Dog類對象的地址是一致的。

再看一個比較有意思的例子:

創(chuàng)建一個NSObject的category,并且給他添加一個實例方法,如下:

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

這個時候會發(fā)生什么呢?
直接看結(jié)果:

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

文字說明一下整個流程:
SubDog類對象通過ISA尋找SubDog元類對象看是否有方法實現(xiàn)->SubDog元類對象通過supperclass指針尋找Dog元類對象是否有方法實現(xiàn)->Dog元類對象通過supperclass尋找到NSObject元類對象->(特殊)NSObject元類對象的supperclass是NSObject的類對象,NSObject元類對象通過supperclass找到NSObject的類對象,NSObject的類對象里記錄了我們添加的實例方法testNSObjectInstanceFun的實現(xiàn)信息,所以就可以直接調(diào)用成功了。

其實我們都知道,方法的調(diào)用是通過Objc 發(fā)送消息的機(jī)制來進(jìn)行消息傳遞然后調(diào)用的。實際上在消息發(fā)送的時候并沒有標(biāo)記這個消息方法是+號消息還是-號消息(類方法或者實例方法),不管是類方法還是實例方法都是通過方法名去找,所以我們通過類對象直接調(diào)用實例方法是可以實現(xiàn)的。

附一張網(wǎng)絡(luò)圖:


小結(jié):

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容