OC類的原理探究一

前言

在前面的文章中我們知道在對象的isa指針中存儲了類的信息嗤瞎,也知道了Class = isa & ISA_MASK墙歪,今天我們來探索一下類以及元類的繼承鏈與類的數(shù)據(jù)結(jié)構(gòu)。
ISA_MASK(掩碼)

# if __arm64__
#   if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#     define ISA_MASK        0x007ffffffffffff8ULL
#   else
#     define ISA_MASK        0x0000000ffffffff8ULL
elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL

1.指針isa指向流程與類的繼承鏈

1.1 isa指向流程

任何一個對象的isa指向當(dāng)前類贝奇,當(dāng)前類的isa指向元類虹菲,元類的isa指向NSObject(根元類),根元類的isa指向根元類掉瞳。如下圖所示

驗證一下毕源,建立一個ZFPerson類利用llvm打印對象的isa指針指向流程

1.2 類繼承鏈

  • 任何類繼承它的父類,父類繼承根類(大部分都是NSObject)陕习,根類繼承一個nil,
    驗證一下霎褐,建立兩個類ZFStudent與ZFPerson。
@interface ZFPerson:NSObject
@end
@implementation ZFPerson
@end

@interface ZFStudent:ZFPerson
@end
@implementation ZFStudent

@end

從打印結(jié)果可以得到上面結(jié)論

  • 任何類的元類繼承它父類的元類该镣,父類的元類繼承根元類冻璃,根元類繼承根類

配啥說perMeteSupClass是NSOject元類,perMeteSupSupClass是NSOject類呢?


從打印結(jié)果可以得到上面結(jié)論是正確的

2.類的結(jié)構(gòu)

從底層源碼可知道Class的本質(zhì)是一個叫objc_class的結(jié)構(gòu)體指針
typedef struct objc_class *Class;

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
    .....
}

對于著下面這一張圖

3.類的bits探索

3.1獲取bits數(shù)據(jù)

從類結(jié)構(gòu)代碼可以看到objc_class繼承objc_object結(jié)構(gòu)體,private繼承objc_object的成員變量isa

struct objc_class : objc_object {
    // Class ISA;       //繼承至objc_object的isa指針 8個字節(jié)
    Class superclass;   //結(jié)構(gòu)體指針8個字節(jié)
    cache_t cache;      //16個字節(jié)       // formerly cache pointer and vtable
    class_data_bits_t bits; //類的地址偏移32位省艳,就可以得到bits 
}

為啥說cache是16字節(jié)呢歌粥?

struct cache_t {
private:
    //typedef unsigned long           uintptr_t; 占用8字節(jié)
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask; //typedef uint32_t mask_t; 4個字節(jié)
#if __LP64__
            uint16_t                   _flags; //2個字節(jié)
#endif
            uint16_t                   _occupied; //2個字節(jié)
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;//8個字節(jié)
    };
 } 

所以cache是16字節(jié),類的isa指針偏移32字節(jié)就可以得到bits數(shù)據(jù)

3.2 class_rw_t結(jié)構(gòu)探索

從上面的bits數(shù)據(jù)中我們可以得到一個class_rw_t數(shù)據(jù)拍埠,class_rw_t在2020的wwdc大會的runtime視頻有講到失驶,截圖如下。里面包含了方法枣购、屬性嬉探、協(xié)議列表等

并且查看class_rw_t結(jié)構(gòu)體源碼,主要有以下幾個屬性

 struct class_rw_t {
   // Be warned that Symbolication knows the layout of this structure.
   uint32_t flags;
   uint16_t witness;
#if SUPPORT_INDEXED_ISA
   uint16_t index;
#endif

   explicit_atomic<uintptr_t> ro_or_rw_ext;

   Class firstSubclass;
   Class nextSiblingClass;
}

firstSubClass與nextSiblingClass:所有的類都會鏈接成一個樹狀結(jié)構(gòu)棉圈,這是通過使用first subclass和Next Sibling Class指針實現(xiàn)的涩堤,這允許運行時遍歷當(dāng)前使用的所有類。

Demangled Name:swift類會使用demangled name字段分瘾,并且swift類不需要這個字段胎围,除非有東西詢問它們的objective-c名稱時才需要

ro_or_rw_ext:存儲class_ro_t或者class_rw_ext_t信息,下面這張圖片是從蘋果2020wwdc視頻中扒取的

通過查看class_rw_t結(jié)構(gòu)體源碼德召,可以發(fā)現(xiàn)它提供了獲取方法白魂、屬性、協(xié)議等方法

const method_array_t methods() const {
        auto v = get_ro_or_rwe();        
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
        } else {
            return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
        }
}
const property_array_t properties() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
        } else {
            return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
        }
 }

const protocol_array_t protocols() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
        } else {
            return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
        }
 }

下面我們通過提供的方法嘗試獲取一下它的方法上岗、屬性福荸、協(xié)議如下

3.2.1 lldb獲取類方法列表

(lldb) p/x ZFPerson.class  //獲取類地址
(Class) $0 = 0x0000000100002850 ZFPerson
(lldb) p/x 0x0000000100002850 + 0x20 //地址偏移32位獲取bits數(shù)據(jù)
(long) $1 = 0x0000000100002870
(lldb) p (class_data_bits_t*)0x0000000100002870 //強制轉(zhuǎn)換bits為class_data_bits_t指針
(class_data_bits_t *) $2 = 0x0000000100002870
(lldb) p $2->data() //獲取class_rw_t 數(shù)據(jù),data方法是結(jié)構(gòu)體class_data_bits_t提供的肴掷,具體可以看源碼
(class_rw_t *) $3 = 0x000000010060e620
(lldb) p *$3
(class_rw_t) $4 = {
  flags = 2148007936
  witness = 0
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = 4294976080
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
}
(lldb) p $4.methods() //獲取方法列表
(const method_array_t) $5 = {
  list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
     = {
      list = {
        ptr = 0x0000000100002298
      }
      arrayAndFlag = 4294976152
    }
  }
}
(lldb) p $5.list.ptr
(method_list_t *const) $6 = 0x0000000100002298
(lldb) p *$6
(method_list_t) $7 = {
  entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 7)
}
(lldb) p $7.get(0)
(method_t) $8 = {}
(lldb) p $7.get(2)
(method_t) $9 = {}
(lldb) p $7.get(1)
(method_t) $10 = {}
(lldb) p $7.get(0).big
(method_t::big) $11 = {
  name = "killPig" //sel
  types = 0x0000000100001ed7 "v16@0:8" //類型編碼敬锐,
  imp = 0x0000000100001b10 (ZFObjcBuild`-[ZFPerson killPig])
}
  Fix-it applied, fixed expression was: 
    $7.get(0).big()
(lldb) p $7.get(1).big
(method_t::big) $12 = {
  name = "eat"
  types = 0x0000000100001ed7 "v16@0:8"
  imp = 0x0000000100001b00 (ZFObjcBuild`-[ZFPerson eat])
}
  Fix-it applied, fixed expression was: 
    $7.get(1).big()
(lldb) p $7.get(2).big
(method_t::big) $13 = {
  name = "init"
  types = 0x0000000100001ecf "@16@0:8"
  imp = 0x0000000100001aa0 (ZFObjcBuild`-[ZFPerson init])
}
  Fix-it applied, fixed expression was: 
    $7.get(2).big()
(lldb) p $7.get(3).big
(method_t::big) $14 = {
  name = "name"
  types = 0x0000000100001ecf "@16@0:8"
  imp = 0x0000000100001b20 (ZFObjcBuild`-[ZFPerson name])
}
  Fix-it applied, fixed expression was: 
    $7.get(3).big()
(lldb) p $7.get(4).big
(method_t::big) $15 = {
  name = "setName:"
  types = 0x0000000100001edf "v24@0:8@16"
  imp = 0x0000000100001b50 (ZFObjcBuild`-[ZFPerson setName:])
}
  Fix-it applied, fixed expression was: 
    $7.get(4).big()
(lldb) p $7.get(5).big
(method_t::big) $16 = {
  name = "age"
  types = 0x0000000100001f91 "i16@0:8"
  imp = 0x0000000100001b80 (ZFObjcBuild`-[ZFPerson age])
}
  Fix-it applied, fixed expression was: 
    $7.get(5).big()
(lldb) p $7.get(6).big
(method_t::big) $17 = {
  name = "setAge:"
  types = 0x0000000100001f99 "v20@0:8i16"
  imp = 0x0000000100001ba0 (ZFObjcBuild`-[ZFPerson setAge:])
}
  Fix-it applied, fixed expression was: 
    $7.get(6).big()
(lldb) 

method_t中的結(jié)構(gòu)以init方法舉例

name = "init" //方法的名稱(sel),選擇器是字符串,它具有唯一性呆瞻,所以它們可以使用指針相等來進行比較

types = 0x0000000100001ecf "@16@0:8" //類型編碼:表示參數(shù)和返回類型的字符串台夺,它不是用來發(fā)送消息的,但它是運行時introspection和消息forwarding所必需的

//指向方法的實現(xiàn)的指針
imp = 0x0000000100001aa0 (ZFObjcBuild`-[ZFPerson init])

從上面可以看到我們輸出的方法列表中是沒有類方法痴脾,因為類方法是存儲在類的元類中
在ZFPerson聲明一個類方法sleep,驗證的具體步驟我就不寫了颤介,跟上面獲取lldb差不多,只是用類的元類地址去做偏移


為什么類方法會放在元類中呢明郭?這是為了避免與同名的對象方法產(chǎn)生沖突买窟,畢竟方法在底層存儲方式就是sel與imp

3.2.2 lldb獲取類的屬性列表

3.2.3 lldb獲取類的協(xié)議列表

聲明了一個ZFPersonDelegate協(xié)議,讓ZFPerson遵循它

@protocol ZFPersonDelegate<NSObject>
- (void)killPig;
@end
(lldb) p/x ZFPerson.class
(Class) $13 = 0x0000000100002850 ZFPerson
(lldb) p 0x0000000100002850 + 0x20
(long) $14 = 4294977648
(lldb) p (class_data_bits_t*)4294977648
(class_data_bits_t *) $15 = 0x0000000100002870
(lldb) p $15->data()
(class_rw_t *) $16 = 0x0000000100627ba0
(lldb) p *$16
(class_rw_t) $17 = {
  flags = 2148007936
  witness = 0
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = 4294976080
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
}
(lldb) p $17.protocols()
(const protocol_array_t) $18 = {
  list_array_tt<unsigned long, protocol_list_t, RawPtr> = {
     = {
      list = {
        ptr = 0x0000000100002348
      }
      arrayAndFlag = 4294976328
    }
  }
}
(lldb) p $18.list
(RawPtr<protocol_list_t>) $19 = {
  ptr = 0x0000000100002348
}
(lldb) p $19.ptr
(protocol_list_t *const) $20 = 0x0000000100002348
(lldb) p *$20
(protocol_list_t) $21 = (count = 1, list = protocol_ref_t [] @ 0x00007fd025b50828)
(lldb) p $21.list[0]
(protocol_ref_t) $22 = 4294977696
(lldb) p/x 4294977696
(long) $23 = 0x00000001000028a0
(lldb) p (protocol_t *)0x00000001000028a0
(protocol_t *) $24 = 0x00000001000028a0
(lldb) p *$24
(protocol_t) $25 = {
  objc_object = {
    isa = {
      bits = 4298453192
      cls = Protocol
       = {
        nonpointer = 0
        has_assoc = 0
        has_cxx_dtor = 0
        shiftcls = 537306649
        magic = 0
        weakly_referenced = 0
        unused = 0
        has_sidetable_rc = 0
        extra_rc = 0
      }
    }
  }
  mangledName = 0x0000000100001ea9 "ZFPersonDelegate"
  protocols = 0x0000000100002430
  instanceMethods = 0x0000000100002448
  classMethods = 0x0000000000000000
  optionalInstanceMethods = 0x0000000000000000
  optionalClassMethods = 0x0000000000000000
  instanceProperties = 0x0000000000000000
  size = 96
  flags = 0
  _extendedMethodTypes = 0x0000000100002468
  _demangledName = 0x0000000000000000
  _classProperties = 0x0000000000000000
}
(lldb) 

通過上面lldb步驟,輸出了class_rw_t結(jié)構(gòu)中的protocol信息

4.補充

4.1 類型編碼
獲取方法的類型編碼 method_getTypeEncoding

蘋果關(guān)于類型編碼文檔

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市拔恰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌亏推,老刑警劉巖学赛,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異吞杭,居然都是意外死亡盏浇,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門芽狗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來绢掰,“玉大人,你說我怎么就攤上這事童擎〉尉ⅲ” “怎么了?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵顾复,是天一觀的道長班挖。 經(jīng)常有香客問我,道長芯砸,這世上最難降的妖魔是什么萧芙? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮假丧,結(jié)果婚禮上双揪,老公的妹妹穿的比我還像新娘。我一直安慰自己虎谢,他們只是感情好盟榴,可當(dāng)我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著婴噩,像睡著了一般。 火紅的嫁衣襯著肌膚如雪羽德。 梳的紋絲不亂的頭發(fā)上几莽,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天,我揣著相機與錄音宅静,去河邊找鬼章蚣。 笑死,一個胖子當(dāng)著我的面吹牛姨夹,可吹牛的內(nèi)容都是我干的纤垂。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼磷账,長吁一口氣:“原來是場噩夢啊……” “哼峭沦!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起逃糟,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤吼鱼,失蹤者是張志新(化名)和其女友劉穎蓬豁,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體菇肃,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡地粪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了琐谤。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蟆技。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖斗忌,靈堂內(nèi)的尸體忽然破棺而出付魔,到底是詐尸還是另有隱情,我是刑警寧澤飞蹂,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布几苍,位于F島的核電站,受9級特大地震影響陈哑,放射性物質(zhì)發(fā)生泄漏妻坝。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一惊窖、第九天 我趴在偏房一處隱蔽的房頂上張望刽宪。 院中可真熱鬧,春花似錦界酒、人聲如沸圣拄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽庇谆。三九已至,卻和暖如春凭疮,著一層夾襖步出監(jiān)牢的瞬間饭耳,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工执解, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留寞肖,地道東北人。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓衰腌,卻偏偏與公主長得像新蟆,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子右蕊,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,834評論 2 345

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