OC對象的底層結(jié)構(gòu)及isa膝捞、superClass詳解

  • isa指針
    通過上一篇文章的分析我們已經(jīng)知道了實(shí)例對象,類對象,元類對象的結(jié)構(gòu)如上圖所示,每個對象中都有 isa 指針,isa 指針有什么作用?他們之間的關(guān)系是怎樣的呢?我們寫一個Person類然后調(diào)用它的實(shí)例方法eat,看一下底層源碼:
[person eat];
//本質(zhì)
objc_msgSend(person, sel_registerName("eat"));

[person eat]本質(zhì)就是向person對象發(fā)送一條eat消息,通過上面的結(jié)構(gòu)圖我們知道,對象方法是存放在類對象中的,并不在實(shí)例對象中,那實(shí)例對象是怎么拿到類對象中的對象方法的呢?同樣,類方法也是如此怎么拿到存放在元類對象中的類方法的呢?.其實(shí)這就用到了isa指針,大家記住:實(shí)例對象的isa指針指向類對象,類對象的isa指針指向元類對象,元類對象的isa指針指向基類的元類對象

  • superClass
    superClass 只存在類對象和元類對象中,實(shí)例對象中沒有superClass.類對象的 superClass 指向父類的類對象,父類的 superClass 指向基類的類對象,如果基類沒有父類,superClass 為nil;元類的 superClass 指向父類的元類對象,父類的元類對象的 superClass 指向基類的元類對象,基類的元類對象的 superClass 指向基類.
    現(xiàn)在我們再添加一個Student類繼承自Person類,如下:
// Person
@interface Person : NSObject <NSCopying>
{
int _age;
int _no;
int _height;
}

-(void)personInstanceMethod;
+(void)personClassMethod;

@end

@implementation Person

- (void)personInstanceMethod{
    
}

+ (void)personClassMethod{
    
}
@end

// Student
@interface Student : Person <NSCopying>
{
    int _sex;
}

-(void)studentInstanceMethod;
+(void)studentClassMethod;

@end

@implementation Student

- (void)studentInstanceMethod{
    
}

+ (void)studentClassMethod{
    
}
@end

然后再創(chuàng)建一個student對象,讓student對象去調(diào)用Person的實(shí)例方法:

Student *student = [[Student alloc]init];
[student personInstanceMethod];

我們知道Person的實(shí)例方法是存放在Person的類對象中的,student對象如何找到這個方法呢?我們畫一張圖:


首先student根據(jù)isa找到Student class對象,然后再通過Student class對象的superClass找到父類Person class對象,再從Person class對象中找到Person的實(shí)例方法.如果父類Person class中依然沒有,就依次往上找,直到NSObject,如果NSObject中還沒有,就會報(bào)一個很經(jīng)典的錯誤unrecognized selector sent to instance.
我們再改變一下,讓改一下代碼,讓Student去執(zhí)行Person的類方法:

[Student personClassMethod];

這次方法執(zhí)行的查找順序是怎樣的呢?我就不畫圖了,其實(shí)跟[student personInstanceMethod]差不多.首先Student類對象先通過isa找到Student 元類對象,然后通過Student 元類對象superClass找到父類的元類對象,也就是Person 元類對象,然后在Person 元類對象找到類方法執(zhí)行.
關(guān)于isa 和 superClass的關(guān)系,網(wǎng)上有人總結(jié)了一張圖,可以很明顯的展示,虛線表示 isa , 實(shí)現(xiàn) 表示 superClass:

我們參照這張圖,對isa 和 superClass做一個總結(jié):

  • instance 的 isa 指向 class , class 的 isa 指向 meta-class , meta-class 的 isa 指向基類的 meta-class.
  • class 的 superClass 指向父類
    如果沒有父類, superClass 為 nil.
  • meta-class 的 superClass 指向父類的 meta-class
    基類的 meta-class 的 superClass 指向基類的 class.
  • instance 調(diào)用方法的軌跡
    通過 isa 找到 class ,如果方法不存在就通過 superClass 找父類
  • class 調(diào)用類的方法軌跡
    通過 isa 找到 meta-class,如果沒有,通過 superClass 找到父類的 meta-class.

注意上圖中有一根 superClass 線我用紅色的??標(biāo)識出來了,這根線特別特殊:基類的元類對象的 superClass 指向基類.下面我們通過代碼驗(yàn)證一下.
我們給NSObject創(chuàng)建一個分類NSObject+test,然后在分類的.m方法中寫一個實(shí)例方法,打印輸出調(diào)用者:

- (void)test{
    NSLog(@"NSObject 的對象方法test,調(diào)用者: %p",self);
}

然后在Person類的頭文件中聲明一個+ (void)test;方法,在讓Person調(diào)用這個方法,如下:

NSLog(@"Student 地址: %p",[Student class]);
NSLog(@"Person 地址: %p",[Person class]);
[Student test];

我們來分析一下,Person的頭文件中聲明了一個test方法,并沒有在實(shí)現(xiàn)文件中實(shí)現(xiàn),而NSObject+test.m文件中實(shí)現(xiàn)了一個對象方法test,如果我們執(zhí)行[Student test];會執(zhí)行成功么?
運(yùn)行一下看看:

2019-02-27 15:23:19.834089+0800 OC對象的分類_01[2112:300025] Student 地址: 0x100001478
2019-02-27 15:23:19.834469+0800 OC對象的分類_01[2112:300025] Person 地址: 0x100001428
2019-02-27 15:23:19.834505+0800 OC對象的分類_01[2112:300025] NSObject 的對象方法test,調(diào)用者: 0x100001478

會發(fā)現(xiàn)并沒有報(bào)unrecognized selector sent to instance這個錯誤,居然調(diào)用成功了!很奇怪,為什么我們使用Student調(diào)用?方法,最后卻執(zhí)行了NSObject的?方法呢?這就是我在上圖中用紅??標(biāo)識的哪條線的作用.

我們分析一下它的方法調(diào)用軌跡:
1: 首先Student通過isa指針找到Studentmeta-class,在Studentmeta-class中查找test方法,結(jié)果沒找到.
2: 又通過Studentmeta-class中的superClass找到他的父類的元類對象,也就是Personmeta-class,結(jié)果又沒找到
3: 繼續(xù)通過Personmeta-class中的superClass找到NSObjectmeta-class,結(jié)果還沒找到
4: 最后NSObjectmeta-classsuperClass指向了基類NSObject,它又去NSObject中查找,結(jié)果找到了- (void)test,就執(zhí)行了.

大家可能會疑惑,為什么調(diào)用的類方法,最后卻執(zhí)行了對象方法?
因?yàn)镺C調(diào)用方法的本質(zhì)就是發(fā)送消息,[Student test]本質(zhì)就是objc_msgSend(objc_getClass("Student"), sel_registerName("test"));它只知道去執(zhí)行test,并不關(guān)心是加號方法還是減號方法.

我們一直在說實(shí)例對象的isa指針指向類對象,類對象的isa指針指向元類對象,也就是說實(shí)例對象的isa地址和類對象的地址相同,類對象的isa地址和元類的地址相同,我們來驗(yàn)證一下.
1: 分別創(chuàng)建Person的實(shí)例對象,類對象,元類對象:

//實(shí)例對象
Person *person = [[Person alloc]init];
//類對象
Class personClass = [person class];
//元類對象
Class personMetaClass = object_getClass(personClass);

2: 通過命令行打印person->isapersonClass地址


person->isa地址是: 0x001d800100001429
personClass地址是: 0x0000000100001428
咦~怎么不一樣呢?
這里需要注意一下,在 arm64 位之前,實(shí)例對象的 isa 地址和類對象的地址就是相同的,但是 arm64 位之后,實(shí)例對象的 isa 地址需要按位與ISA_MASK后才能得到類對象的地址,我們在object之isa指針詳解篇幅中詳細(xì)講解過,有興趣的同學(xué)可以看一下.
ISA_MASK是什么呢?打開 runtime 源碼搜索一下就能看到:

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL

# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL

如果是 arm64 位系統(tǒng),ISA_MASK就是0x0000000ffffffff8ULL,如果是x86_64系統(tǒng)ISA_MASK就是0x00007ffffffffff8ULL,因?yàn)槲覀兪窃趍ac 上運(yùn)行,所以我們就使用x86_64的:

計(jì)算結(jié)果

可以發(fā)現(xiàn),通過位運(yùn)算后得到的地址0x0000000100001428personClass地址:0x0000000100001428就是同一個地址!
下面驗(yàn)證類對象的 isa 指針指向元類對象
我們打印personClass 的 isa,會發(fā)現(xiàn)報(bào)錯,因?yàn)橄到y(tǒng)沒有暴露isa這個成員:
image.png

為了解決這個問題,我們定義一個和class結(jié)構(gòu)相同的hh_objc_class:

struct objc_class {
    Class _Nonnull isa;
    Class _Nullable super_class
}

然后把Class personClass類型轉(zhuǎn)換為struct hh_objc_class類型:

struct hh_objc_class *hh_personClass = (__bridge struct hh_objc_class *)(personClass);

然后再打印:
p/x hh_personClass->isa發(fā)現(xiàn)結(jié)果為:0x001d800100001401
p/x personMetaClass的結(jié)果為:0x0000000100001400
同樣在按位或運(yùn)算:


按位或運(yùn)算后的結(jié)果和p/x personMetaClass的結(jié)果:0x0000000100001400相同,也印證了我們之前的結(jié)論.

現(xiàn)在來驗(yàn)證一下類對象的 superClass 指向父類對象也就是說Student classsuperClass地址和Person class的地址相同.
還是之前代碼,我們直接打印p/x hh_studentClass->super_classp/x personClass,結(jié)果如圖:

通過打印的結(jié)果可以看出,superClassisa不同,superClass并沒有& ISA_MASK,而是直接相等.

我們一直在說屬性信息见咒、協(xié)議信息榜晦、成員變量信息获茬、對象方法存放在類對象中,類方法存放在元類對象中,但是一直沒有親眼看到,接下來我們就來驗(yàn)證一下,親眼看看.
首先我們知道,類對象和元類對象的結(jié)構(gòu)是一模一樣的,因?yàn)樗麄兌际?code>class類型.所以我們點(diǎn)擊進(jìn)入class類型,發(fā)現(xiàn)它是這樣:

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

可以看到這個結(jié)構(gòu)體中的確是有method_list,ivar_list,protocol_list等等,完全符合我們之前的說法,但是請注意:#if !__OBJC2__,也就是說這段代碼的執(zhí)行有一個前提不是objc2.0才會執(zhí)行,而現(xiàn)在我們用的都是OC2.0了,所以這段代碼是不會執(zhí)行的.也就是說這段代碼已經(jīng)過時了OBJC2_UNAVAILABLE.所以我們需要從 runtime 的源碼中查看.
打開 runtime 源碼搜索struct objc_class找到struct objc_class : objc_object {這個結(jié)構(gòu)體,我把主要成員挑出來,如下:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags 獲取具體的類信息

    class_rw_t *data() { 
        return bits.data();
    }
}

我們主要研究class_rw_t,rw_tread_write_table的縮寫,意思是可讀可寫的表.bits.data()返回class_rw_t.class_rw_t里面存放的什么信息呢?我們點(diǎn)擊進(jìn)入class_rw_t看看:

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;//方法信息
    property_array_t properties;//屬性信息
    protocol_array_t protocols;//協(xié)議信息
}

可以看到我們想看的方法,屬性,協(xié)議信息.只是還沒看到成員變量信息. class_rw_t中有一個class_ro_t,ro_treadOnly_table的縮寫,意思是只讀的表.我們點(diǎn)擊進(jìn)入const class_ro_t看看里面存放哪些信息:

struct class_ro_t {
    const char * name;//類名
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;//成員變量
}

我畫了一張圖,能一目了然展示Class的內(nèi)存結(jié)構(gòu):

Class類和元類內(nèi)存結(jié)構(gòu)圖

這就是class類型在 runtime 中的源碼,到這里我們已經(jīng)看到了所有我們想到看的信息,但是這只是在源碼上證明了結(jié)論,最好我們能把代碼跑起來,在內(nèi)存中看看是否是這樣,要想做到這種效果,就要借助李哥用 c++ 仿寫的一個class類型:MJClassInfo.h文件.
步驟:
1: 導(dǎo)入MJClassInfo.h文件到我們工程中,這是編譯會出錯,我們把main.m后綴改為main.mm,讓編譯器編譯OC 和 C++ 文件.
2: 創(chuàng)建Student classPerson class然后轉(zhuǎn)換為mj_objc_class類型

mj_objc_class *studentClass = (__bridge mj_objc_class *)[Student class];
mj_objc_class *personClass = (__bridge mj_objc_class *)[Person class];

3: 調(diào)用mj_objc_classdata()方法,返回class_rw_t,這是李哥封裝的方法,在data()內(nèi)部調(diào)用bits.data()和runtime源碼實(shí)現(xiàn)一致.

class_rw_t *studentClass_rw_t_Data = studentClass->data();
class_rw_t *personClass_rw_t_Data = personClass->data();

4:打斷點(diǎn),查看class_rw_t內(nèi)存數(shù)據(jù):


這樣我們也從內(nèi)存上查驗(yàn)了class的內(nèi)存結(jié)構(gòu),meta-class的查驗(yàn)方法和class一樣,我就不寫步驟了,直接上圖大家擼一眼:

大總結(jié):

1: 對象的 isa 指針指向哪里?

  • instance 對象的 isa 指針指向 class 對象
  • class 對象的 isa 指針指向 meta-class對象
  • meta-class 對象的 isa 指向基類的 meta-class 對象

2: OC的類信息存放在哪里?

  • 屬性信息,協(xié)議信息,成員變量信息,對象方法存放在類對象中.
  • 類方法存放在 meta-class 對象中.
  • 成員變量的具體值存放在 instance 對象.

3: 類對象和元類對象什么時候加載?什么時候釋放?

  • 當(dāng)程序執(zhí)行main函數(shù)后就會加載類信息,此時加載到內(nèi)存;
  • 程序運(yùn)行過程中一直存在,直到程序退出時銷毀.

4: 實(shí)例對象中的成員變量和類對象中的成員變量有什么不同?

  • 實(shí)例對象中存儲的成員變量的具體值
  • 類對象中的存儲的成員變量的類型,名字等信息,只存儲一份
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市甸祭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌褥影,老刑警劉巖池户,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異凡怎,居然都是意外死亡校焦,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門统倒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來寨典,“玉大人,你說我怎么就攤上這事房匆∷食桑” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵浴鸿,是天一觀的道長井氢。 經(jīng)常有香客問我,道長赚楚,這世上最難降的妖魔是什么毙沾? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮宠页,結(jié)果婚禮上左胞,老公的妹妹穿的比我還像新娘。我一直安慰自己举户,他們只是感情好烤宙,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著俭嘁,像睡著了一般躺枕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天拐云,我揣著相機(jī)與錄音罢猪,去河邊找鬼。 笑死叉瘩,一個胖子當(dāng)著我的面吹牛膳帕,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播薇缅,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼危彩,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了泳桦?” 一聲冷哼從身側(cè)響起汤徽,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎灸撰,沒想到半個月后谒府,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡梧奢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年狱掂,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片亲轨。...
    茶點(diǎn)故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡趋惨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出惦蚊,到底是詐尸還是另有隱情器虾,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布蹦锋,位于F島的核電站兆沙,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏莉掂。R本人自食惡果不足惜葛圃,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望憎妙。 院中可真熱鬧库正,春花似錦、人聲如沸厘唾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽抚垃。三九已至喷楣,卻和暖如春趟大,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背铣焊。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工逊朽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人曲伊。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓惋耙,卻偏偏與公主長得像,于是被迫代替她去往敵國和親熊昌。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評論 2 353

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