六 OC 底層原理 類的結(jié)構(gòu)

前言

當(dāng)一個相同的類創(chuàng)建多個對象時,那么這么對象的類是不是創(chuàng)建了多個呢

Class d1 = [Desk class];
Class d2 = [Desk alloc].class;
Class d3 =  object_getClass([Desk alloc]);
Class d4 = [Desk alloc].class;
NSLog(@"%p - %p - %p - %p", d1, d2, d3, d4);

--------------
Room[42703:2055484] 0x1000080f0 - 0x1000080f0 - 0x1000080f0 - 0x1000080f0

類對象地址顯然相同劲藐, 所以 類在內(nèi)存空間中只存了一份

回顧

上篇我們知道類在底層一個繼承于 objc_object 的對象,也就是object_class 結(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() const {
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }

    void setInfo(uint32_t set) {
        ASSERT(isFuture()  ||  isRealized());
        data()->setFlags(set);
    }
}

我們知道 objc_class 第一個內(nèi)存是 isasuperclass 存儲的父類信息奴饮,占據(jù)8個字節(jié),cache 緩存占用 16 字節(jié)择浊, bits 存儲了類的方法拐云,屬性,協(xié)議等信息的地方近她,具體的注釋表示 class_data_bits_t 相當(dāng)于 class_rw_t 指針加上 rr/alloc 的標(biāo)識

class_data_bits_t結(jié)構(gòu)體返回了 class_rw_t 指針,那 class_rw_t 又是什么呢?

就在代碼旁簡單謝謝介紹

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;
    // 當(dāng)前所屬類 第一個子類
    Class firstSubclass;
    // 當(dāng)前所有類 的兄弟類
    Class nextSiblingClass;

private:
    // ro_or_rw_ext_t會有兩種情況:
    // 1): 值是 class_ro_t *
    // 2): 值是 class_rw_ext_t *叉瘩,
    //          而 class_ro_t * 作為 class_rw_ext_t 的 const class_ro_t *ro 成員變量保存
    using ro_or_rw_ext_t = objc::PointerUnion<const class_ro_t *, class_rw_ext_t *>;
    const ro_or_rw_ext_t get_ro_or_rwe() const {
          return ro_or_rw_ext_t{ro_or_rw_ext};
    }
    ... 此處省略
}
// class_rw_ext_t 其中 rw 可以理解為 read write
struct class_rw_ext_t {
    // 存放只讀類型
    const class_ro_t *ro;
    // 方法列表
    method_array_t methods;
    // 屬性列表
    property_array_t properties;
    // 協(xié)議列表
    protocol_array_t protocols;
    // 所屬類名
    char *demangledName;
    // 版本號
    uint32_t version;
};

// class_ro_t 其中 ro 可以理解為 read only
struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
    ... 此處省略
};

可以看出 class_rw_t 存放者類的屬性 協(xié)議 方法等信息

現(xiàn)在我們驗證一下類的成員變量 屬性存儲的位置

準(zhǔn)備

@interface TObject : NSObject
{
    NSString *tname;
}
@property (nonatomic, copy) NSString* toName;

+ (void) classMethodTest;
- (void) instanceMethodTest;
@end

@implementation TObject
+(void)classMethodTest{
    NSLog(@"%s",__func__);
}
-(void)instanceMethodTest{
    NSLog(@"%s",__func__);
}
@end

成員變量 / 屬性 存儲的位置

接下我 我們就要利用 lldb 調(diào)試 一步一步驗證

  1. 我們先要獲取類對象的內(nèi)存地址,通過類結(jié)構(gòu)分析 class_data_bits_t bits 字段之前 還有32 字節(jié)
    所以我們需要通過內(nèi)存便宜獲取 bits的內(nèi)存地址
    在這我們介紹一個命令 x/5gx 其中第一個 x 表示讀取內(nèi)存粘捎,5表示分成5段薇缅,g 表示一個單元8個字節(jié)危彩,x 表示16進(jìn)制, 所以其中第5個地址 就是 bits 的地址
(lldb) p obj1.class
(Class) $0 = TObject
(lldb) x/5gx $0
0x1000021f0: 0x00000001000021c8 0x0000000100333140
0x100002200: 0x00000001018335c0 0x0001802400000007
0x100002210: 0x00000001006656a4
(lldb) 
  1. 我們現(xiàn)在需要將 地址強(qiáng)轉(zhuǎn)為 class_data_bits_t 類型,并通過 data() 函數(shù) 獲取 class_rw_t
(lldb) p (class_data_bits_t*)0x100002210
(class_data_bits_t *) $1 = 0x0000000100002210
(lldb) p $1->data()
(class_rw_t *) $2 = 0x000020a000000000
(lldb) p *$2
(class_rw_t) $3 = {
  flags = 2148007936
  witness = 1
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = 4294975648
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
}
  1. 此時我們調(diào)用class_rw_t 結(jié)構(gòu)體中的 properties() 獲取屬性列表,可以看到屬性名稱 toName
(lldb) p $3.properties()
(const property_array_t) $4 = {
  list_array_tt<property_t, property_list_t> = {
     = {
      list = 0x0000000100002198
      arrayAndFlag = 4294975896
    }
  }
}
(lldb) p $4.list
(property_list_t *const) $5 = 0x0000000100002198
p *$5
(property_list_t) $6 = {
  entsize_list_tt<property_t, property_list_t, 0> = {
    entsizeAndFlags = 16
    count = 1
    first = (name = "toName", attributes = "T@\"NSString\",C,N,V_toName") // 其中 C 代Copy泳桦,N 代表 nonatomic
  }
}
  1. 此時 我們的成員變量又去哪了呢汤徽,接下來 我們接著調(diào)用 class_rw_t 的 ro屬性,并獲取 ro 的 ivar
(lldb) p $3.ro()
(const class_ro_t *) $9 = 0x00000001000020a0
(lldb) p $9->ivars
(const ivar_list_t *const) $10 = 0x0000000100002150
(lldb) p *$10
(const ivar_list_t) $12 = {
  entsize_list_tt<ivar_t, ivar_list_t, 0> = {
    entsizeAndFlags = 32
    count = 2
    first = {
      offset = 0x00000001000021c0  // 偏移量
      name = 0x0000000100000eb9 "tname" // 名稱
      type = 0x0000000100000f8a "@\"NSString\"" // 類型
      alignment_raw = 3 // 8字節(jié)對齊
      size = 8 // 內(nèi)存占用大小
    }
  }
}

由此可見 我們的 屬性是存在 class_rw_t 中的 屬性列表中的,而成員變量是存儲在 class_ro_t 中的 ivars ,成員變量列表中的

實例方法 / 類方法 的存儲位置

  1. 接著上面的調(diào)用灸撰,我們先獲取class_rw_t的方法列表 methods(),在讀取 列表信息
(lldb) p $3.methods()
(const method_array_t) $7 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x00000001000020e0
      arrayAndFlag = 4294975712
    }
  }
}
(lldb) p $7.list
(method_list_t *const) $8 = 0x00000001000020e0
(lldb) p *$8
(method_list_t) $9 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 3
    first = {
      name = "instanceMethodTest"
      types = 0x0000000100000f34 "v16@0:8"
      imp = 0x0000000100000db0 (Objc_alloc`-[TObject instanceMethodTest])
    }
  }
}
(lldb) 

由此可見我們的實例方法確實是存儲在 class_rw_t 中的方法列表中的

  1. 現(xiàn)在找找我們的類方法存到哪了谒府, 先看看 class_ro_t 中的baseMethodList
lldb) p $3.ro()
(const class_ro_t *) $10 = 0x0000000100002098
(lldb) p $10->baseMethodList
(method_list_t *const) $12 = 0x00000001000020e0
(lldb) p *$12
(method_list_t) $13 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 3
    first = {
      name = "instanceMethodTest"
      types = 0x0000000100000f34 "v16@0:8"
      imp = 0x0000000100000db0 (Objc_alloc`-[TObject instanceMethodTest])
    }
  }
}
(lldb) 

我們看到 在 class_ro_t 中的方法列表中,還是存儲著我們的實例方法信息
那我們的類方法到底去哪了呢浮毯,通過 isa 的繼承關(guān)系 推導(dǎo)完疫,會不會在我們的元類中呢

  1. 現(xiàn)在我們開始獲取元類地址,然后獲取元類的 class_rw_t 中的方法列表中债蓝,流程和上面的是一樣的
(lldb) p objc_getMetaClass(object_getClassName(obj1))
(Class) $14 = 0x00000001000021a8
(lldb) x/5gx $14
0x1000021a8: 0x00000001003330f0 0x00000001003330f0
0x1000021b8: 0x0000000101925020 0x0001e03500000007
0x1000021c8: 0x00000001006ce2e4
(lldb) p (class_data_bits_t*)0x1000021c8
(class_data_bits_t *) $15 = 0x00000001000021c8
(lldb) p $15->data()
(class_rw_t *) $17 = 0x00000001006ce2e0
(lldb) p *$17
(class_rw_t) $18 = {
  flags = 2684878849
  witness = 1
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = 4294975536
  }
  firstSubclass = nil
  nextSiblingClass = 0x00007fff89e08cf8
}
(lldb) p $18.methods
(const method_array_t) $19 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x0000000100002078
      arrayAndFlag = 4294975608
    }
  }
}
  Fix-it applied, fixed expression was: 
    $18.methods()
(lldb) p $19.list
(method_list_t *const) $20 = 0x0000000100002078
(lldb) p *$20
(method_list_t) $21 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 1
    first = {
      name = "classMethodTest"
      types = 0x0000000100000f34 "v16@0:8"
      imp = 0x0000000100000d80 (Objc_alloc`+[TObject classMethodTest])
    }
  }
}
(lldb) 

由此可見 我們的類方法是存儲在對象所屬類的元類當(dāng)中的方法列表

總結(jié)

這是一張類的結(jié)構(gòu)圖


image.png
  • 方法
    • 實例方法確實是存儲在 class_rw_t 中的methods中的
    • 類方法存儲在 當(dāng)前對象所屬類的元類中的 class_rw_t 中的methods
  • 屬性
    • 對象屬性 -> class_rw_t中的properties
    • 成員變量 -> class_rw_t -> class_ro_t *ro -> ivars

補(bǔ)充 - 驗證以上

  1. 例1 通過class_getInstanceMethod 獲取類的實例方法
Class class = TObject.class;
const char *className = class_getName(class);
Class metaClass = objc_getMetaClass(className);
 // ---  class_getInstanceMethod
Method method1 = class_getInstanceMethod(class, @selector(instanceMethodTest));
Method method2 = class_getInstanceMethod(metaClass, @selector(instanceMethodTest));

Method method3 = class_getInstanceMethod(class, @selector(classMethodTest));
Method method4 = class_getInstanceMethod(metaClass, @selector(classMethodTest));
        
NSLog(@"class_getInstanceMethod - %p-%p-%p-%p",method1,method2,method3,method4);
===========
 TObjc[4080:382660] class_getInstanceMethod - 0x100003d51-0x0-0x0-0x100003d39

class_getInstanceMethod返回的是類的實例方法壳鹤,如果類或者父類中沒有找到,就返回nil

/***********************************************************************
* class_getInstanceMethod.  Return the instance method for the
* specified class and selector.
**********************************************************************/
Method class_getInstanceMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    // This deliberately avoids +initialize because it historically did so.

    // This implementation is a bit weird because it's the only place that 
    // wants a Method instead of an IMP.

#warning fixme build and search caches
        
    // Search method lists, try method resolver, etc.
    lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);

#warning fixme build and search caches

    return _class_getMethod(cls, sel);
}

以上 method1 和 method4 是有值的饰迹,進(jìn)一步驗證了 實例方法存在類中芳誓,類方法存在元類中,
也給了我們另一個定義 也就是說 類方法啊鸭,對于元類來說 其實就是一個實例方法

  1. 例2 獲取 類方法 class_getClassMethod
// ---  class_getClassMethod
Method method5 = class_getClassMethod(class, @selector(instanceMethodTest));
Method method6 = class_getClassMethod(metaClass, @selector(instanceMethodTest));

Method method7 = class_getClassMethod(class, @selector(classMethodTest));
Method method8 = class_getClassMethod(metaClass, @selector(classMethodTest));
        
NSLog(@"class_getClassMethod - %p-%p-%p-%p",method5,method6,method7,method8);
========
2020-12-15 21:54:56.966095+0800 TObjc[4080:382660] class_getClassMethod - 0x0-0x0-0x100003d39-0x100003d39

class_getClassMethod 這個方法 其實就是使用元類 獲取實例方法

Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    return class_getInstanceMethod(cls->getMeta(), sel);
}
Class getMeta() {
  if (isMetaClass()) return (Class)this;
  else return this->ISA();
}

其實都是走的class_getInstanceMethod 函數(shù)锹淌,method7 之所以會有值,也是因為 getMeta()這個函數(shù)返回元類的原因赠制, 然而當(dāng) 傳入的是元類的話 就會返回本身葛圃,這也就意味著 method7method8其實是一樣的

3.例3 獲取方法實現(xiàn)class_getMethodImplementation

// 尋找方法實現(xiàn) class_getMethodImplementation
IMP imp1 = class_getMethodImplementation(class, @selector(instanceMethodTest));
IMP imp2 = class_getMethodImplementation(metaClass, @selector(instanceMethodTest));

IMP imp3 = class_getMethodImplementation(class, @selector(classMethodTest));
IMP imp4 = class_getMethodImplementation(metaClass, @selector(classMethodTest));
NSLog(@" class_getMethodImplementation %p-%p-%p-%p",imp1,imp2,imp3,imp4);

=======
2020-12-15 21:54:56.966114+0800 KCObjc[4080:382660]  class_getMethodImplementation 0x100003ab4-0x199ce6600-0x199ce6600-0x100003a7c

其中有一個很重要的東西 _objc_msgForward 消息轉(zhuǎn)發(fā)

IMP class_getMethodImplementation(Class cls, SEL sel)
{
    IMP imp;

    if (!cls  ||  !sel) return nil;

    imp = lookUpImpOrNil(nil, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);

    // Translate forwarding function to C-callable external version
    if (!imp) {
        return _objc_msgForward;
    }

    return imp;
}

所以 imp2imp3 是沒有找到的,只是進(jìn)行了消息轉(zhuǎn)發(fā)憎妙,所以打印都是有值的

class_getInstanceMethod:返回的是類的實例方法,如果在傳入的類或者類的父類中沒有找到指定的實例方法曲楚,則返回空厘唾。
class_getClassMethod:獲取類方法,本質(zhì)上還是調(diào)用 class_getInstanceMethod,不過在這其中的參數(shù) 變成了元類龙誊,如果傳入的類或者父類中 對應(yīng)的元類中沒有找到抚垃,那就返回空
class_getMethodImplementation: 尋找方法實現(xiàn),如果在對應(yīng)的類拐邪,或者元類中沒有找到蜒谤,那就進(jìn)行消息轉(zhuǎn)發(fā)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末毛雇,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子罕伯,更是在濱河造成了極大的恐慌,老刑警劉巖叽讳,帶你破解...
    沈念sama閱讀 216,692評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件追他,死亡現(xiàn)場離奇詭異坟募,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)邑狸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評論 3 392
  • 文/潘曉璐 我一進(jìn)店門懈糯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人单雾,你說我怎么就攤上這事赚哗。” “怎么了硅堆?”我有些...
    開封第一講書人閱讀 162,995評論 0 353
  • 文/不壞的土叔 我叫張陵屿储,是天一觀的道長。 經(jīng)常有香客問我硬萍,道長扩所,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,223評論 1 292
  • 正文 為了忘掉前任朴乖,我火速辦了婚禮祖屏,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘买羞。我一直安慰自己袁勺,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評論 6 388
  • 文/花漫 我一把揭開白布畜普。 她就那樣靜靜地躺著期丰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪吃挑。 梳的紋絲不亂的頭發(fā)上钝荡,一...
    開封第一講書人閱讀 51,208評論 1 299
  • 那天,我揣著相機(jī)與錄音舶衬,去河邊找鬼埠通。 笑死,一個胖子當(dāng)著我的面吹牛逛犹,可吹牛的內(nèi)容都是我干的端辱。 我是一名探鬼主播,決...
    沈念sama閱讀 40,091評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼虽画,長吁一口氣:“原來是場噩夢啊……” “哼舞蔽!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起码撰,我...
    開封第一講書人閱讀 38,929評論 0 274
  • 序言:老撾萬榮一對情侶失蹤渗柿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后脖岛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體做祝,經(jīng)...
    沈念sama閱讀 45,346評論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡砾省,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了混槐。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片编兄。...
    茶點(diǎn)故事閱讀 39,739評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖声登,靈堂內(nèi)的尸體忽然破棺而出狠鸳,到底是詐尸還是另有隱情,我是刑警寧澤悯嗓,帶...
    沈念sama閱讀 35,437評論 5 344
  • 正文 年R本政府宣布件舵,位于F島的核電站,受9級特大地震影響脯厨,放射性物質(zhì)發(fā)生泄漏铅祸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評論 3 326
  • 文/蒙蒙 一合武、第九天 我趴在偏房一處隱蔽的房頂上張望临梗。 院中可真熱鬧,春花似錦稼跳、人聲如沸盟庞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽什猖。三九已至,卻和暖如春红淡,著一層夾襖步出監(jiān)牢的瞬間不狮,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評論 1 269
  • 我被黑心中介騙來泰國打工在旱, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留摇零,地道東北人。 一個月前我還...
    沈念sama閱讀 47,760評論 2 369
  • 正文 我出身青樓颈渊,卻偏偏與公主長得像,于是被迫代替她去往敵國和親终佛。 傳聞我的和親對象是個殘疾皇子俊嗽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評論 2 354

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