iOS底層原理探究 - 類結(jié)構(gòu)分析

前言

等風(fēng)來不如追風(fēng)去,總有那么一個人在這風(fēng)景正好的季節(jié)來到你的身邊雅潭。在上一篇探究了對象的本質(zhì)和isa指針的底層均蜜,那么我們繼續(xù)來看的底層結(jié)構(gòu)担平。

補(bǔ)充知識

  • 在OC環(huán)境下使用的類示绊,在底層都有替換的類去實現(xiàn)
底層實現(xiàn).png

isa 和類的關(guān)聯(lián)

類的isa

探索對象的時候我們已經(jīng)知道,對象的結(jié)構(gòu)體中的isa指向的是暂论,這個時候就會想面褐,也是一個對象,中也有isa取胎,那么isa又指向哪里呢展哭?如下圖:

類和isa的關(guān)聯(lián).png
從圖中可以看出,JCPerson類對應(yīng)了兩個內(nèi)存地址0x00000001000085f00x00000001000085c8平匈,哪一個才是JCPerson的地址呢?一個在內(nèi)存中存在幾個內(nèi)存地址呢泉孩?接下來看下圖:

類的地址.png

看到這里應(yīng)該非常清晰了大州,一個只有一個內(nèi)存地址,0x00000001000085f0JCPerson的類地址漆撞,而0x00000001000085c8這個類地址蘋果把它叫做元類

總結(jié):
  • 元類由系統(tǒng)編譯器自動生成和編譯,與創(chuàng)建者無關(guān)
  • 對象isa指向類對象isa指向元類

isa的走位圖

上面我們已經(jīng)分析了對象映挂、isa的走向,那么元類isa又指向哪里呢盗尸?結(jié)合LLDB來進(jìn)行探索柑船。

LLDB調(diào)試isa走位圖.png
從圖中可以得到:

  • 對象 isa --> 類 isa --> 元類 isa --> 根元類isa --> 根元類(自己)
  • 根類(NSObject) isa --> 根元類 isa --> 根元類(自己)
isa的流程圖:
isa走位圖.png

類、元類泼各、根元類繼承圖

創(chuàng)建JCPerson,JCTeacher,NSObject的相關(guān)代碼鞍时,探究一下它們的繼承關(guān)系。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"%@",class_getSuperclass(JCTeacher.class));
        NSLog(@"%@",class_getSuperclass(JCPerson.class));
        NSLog(@"%@",class_getSuperclass(NSObject.class));
        # NSObject實例對象
        NSObject *object1 = [NSObject alloc];
        # NSObject類
        Class class = object_getClass(object1);
        # NSObject元類
        Class metaClass = object_getClass(class);
        # NSObject根元類
        Class rootMetaClass = object_getClass(metaClass);
        # NSObject根根元類
        Class rootRootMetaClass = object_getClass(rootMetaClass);
        NSLog(@"\nNSObject實例對象  %p\nNSObject類  %p\nNSObject元類  %p\nNSObject根元類  %p\nNSObject根根元類  %p",object1,class,metaClass,rootMetaClass,rootRootMetaClass);
        # NSObject 根類特殊情況
        Class nsuperClass = class_getSuperclass(NSObject.class);
        NSLog(@"%@ - %p",nsuperClass,nsuperClass);
        # 根元類 -> NSObject
        Class rnsuperClass = class_getSuperclass(metaClass);
        NSLog(@"%@ - %p",rnsuperClass,rnsuperClass);
        # JCPerson元類和元類的父類
        Class pMetaClass = object_getClass(JCPerson.class);
        Class psuperClass = class_getSuperclass(pMetaClass);
        NSLog(@"%@ - %p",psuperClass,psuperClass);
        # JCTeacher元類和元類的父類
        Class tMetaClass = object_getClass(JCTeacher.class);
        Class tsuperClass = class_getSuperclass(tMetaClass);
        NSLog(@"%@ - %p",tsuperClass,tsuperClass);
    }
    return 0;
}

類扣蜻、元類的繼承關(guān)系.png

源碼分析中知道逆巍,NSObject的父類打印結(jié)果是nilNSObject根元類的父類的地址等于NSObject類的地址莽使;JCPerson元類的父類的地址等于NSObject的元類锐极。

  • JCTeacher-->JCPerson-->NSObject-->nil
  • JCTeacher元類-->JCPerson元類-->NSObject元類-->NSObject根元類-->NSObject
的繼承圖:
類的繼承圖.png
isa流程圖和繼承鏈:
isa流程圖和繼承圖.png

內(nèi)存偏移

在前面探究對象的底層實現(xiàn),我們了解到對象屬性getter方法底層實現(xiàn)是通過首地址+內(nèi)存偏移的方式去獲取內(nèi)存中的變量芳肌。接下來我們來看一下內(nèi)存偏移灵再。

基本指針
# 普通指針
int a = 10; //
int b = 10; //
JCNSLog(@"%d -- %p",a,&a);
JCNSLog(@"%d -- %p",b,&b);
==========: 10 -- 0x7ffeefbff4ec
==========: 10 -- 0x7ffeefbff4e8
  • a的地址是0x7ffeefbff4ecb的地址是0x7ffeefbff4e8亿笤,相差4個字節(jié)翎迁,int類型是4個字節(jié)的長度
  • a>b的地址,從高地址低地址偏移净薛,這符合棧內(nèi)存的分配原則
對象指針
# 對象
JCPerson *p1 = [JCPerson alloc];
JCPerson *p2 = [JCPerson alloc];
JCNSLog(@"%@ -- %p",p1,&p1);
JCNSLog(@"%@ -- %p",p2,&p2);
==========: <JCPerson: 0x1004075a0> -- 0x7ffeefbff4e8
==========: <JCPerson: 0x100408570> -- 0x7ffeefbff4e0
  • alloc開辟的內(nèi)存在堆區(qū)汪榔,指針地址棧區(qū)
  • 堆區(qū)是從地址 --> 地址,棧區(qū)是從地址 --> 地址
數(shù)組指針
int c[4] = {1,2,3,4};
int *d   = c;
JCNSLog(@"%p - %p - %p",&c,&c[0],&c[1]);
JCNSLog(@"%p - %p - %p",d,d+1,d+2);

for (int i = 0; i<4; i++) {
     int value =  *(d+i);
     JCNSLog(@"----%d",value);
}
==========: 0x7ffeefbff4e0 - 0x7ffeefbff4e0 - 0x7ffeefbff4e4
==========: 0x7ffeefbff4e0 - 0x7ffeefbff4e4 - 0x7ffeefbff4e8
==========: ----1
==========: ----2
==========: ----3
==========: ----4
  • 數(shù)組的地址就是元素的首地址罕拂,即&c == &c[0]
  • 數(shù)組中每個元素的地址可以通過:首地址 + n*元素類型大小來獲取揍异,只需要數(shù)組中元素數(shù)據(jù)類型相同
  • 數(shù)組中的每個元素的地址間隔是通過當(dāng)前元素的數(shù)據(jù)類型決定的
總結(jié):
  • 內(nèi)存偏移可以根據(jù)首地址 + 偏移值方式來獲取各個數(shù)據(jù)的內(nèi)存地址

類結(jié)構(gòu)的分析

上面我們已經(jīng)了解了內(nèi)存偏移的知識,接下來我們來探究的底層結(jié)構(gòu)爆班。

類的內(nèi)存地址.png
代碼分析:在對象底層結(jié)構(gòu)中存放在屬性衷掷、成員變量等數(shù)據(jù);從圖中打印可以看出是有內(nèi)存的柿菩,那么它里面存放著什么呢戚嗅?接下來分析底層的數(shù)據(jù)結(jié)構(gòu)。

類的底層結(jié)構(gòu):

探索對象isa的過程中,我們已經(jīng)知道isa在底層是Class類型懦胞,Class類型是objc_class *替久,所有的底層實現(xiàn)都是objc_class,通過全局搜索可以找到如下代碼:

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;   # OBJC2不可用

通過上面這段代碼我們發(fā)現(xiàn)在OBJC2中不可用躏尉,而現(xiàn)在的版本基本是在用OBJC2蚯根,所以這不是我們分析的,接下來在看一下如下代碼:

struct objc_class : objc_object {
  objc_class(const objc_class&) = delete;
  objc_class(objc_class&&) = delete;
  void operator=(const objc_class&) = delete;
  void operator=(objc_class&&) = delete;
    // 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
    ...
    下面全部是方法胀糜,不需要分析颅拦,省略
}

源碼分析:objc_class是繼承objc_object,這說明也是對象教藻,所謂萬物皆對象距帅。objc_class里面有一個隱藏成員變量isa,我們前面已經(jīng)分析過了括堤,下面還有三個成員變量superclass,cache,bits碌秸,我們知道首地址就是isa,那么我們可以通過首地址+偏移量的方式去獲取成員變量的地址悄窃,然后獲取值讥电。

  • isa是結(jié)構(gòu)體指針,占8字節(jié)
  • Class superclassClass類型广匙,也是屬于結(jié)構(gòu)體指針允趟,占8字節(jié)
  • cachecache_t結(jié)構(gòu)體,結(jié)構(gòu)體大小由內(nèi)部的變量決定
  • bitsclass_data_bits_t結(jié)構(gòu)體鸦致,如果知道前面三個成員變量的大小潮剪,那么就可以得到bits的地址

前面三個成員變量已經(jīng)知道了前兩個的大小,只要知道cache的內(nèi)存大小分唾,接下來看一下cache_t的內(nèi)存大小

typedef unsigned long           uintptr_t;
#if __LP64__
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;  //8
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask;  //4
#if __LP64__
            uint16_t                   _flags;   //2
#endif
            uint16_t                   _occupied;  //2
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache; //8
    };
   ...
   下面的一些方法直接省略(static類型的是在內(nèi)存的全局區(qū)抗碰,不在結(jié)構(gòu)體里面的內(nèi)存)

}

cache_t是一個結(jié)構(gòu)體類型,內(nèi)部包含了_bucketsAndMaybeMask和一個聯(lián)合體.

  • _bucketsAndMaybeMaskuintptr_t類型绽乔,而uintptr_t是無符號長整型占8個字節(jié)
  • 聯(lián)合體內(nèi)存大小由成員變量中的最大變量的內(nèi)存大小決定弧蝇,該聯(lián)合體由一個結(jié)構(gòu)體_originalPreoptCache兩個成員變量組成,由于聯(lián)合體存在互斥的折砸,所以只需要得到其中最大變量的內(nèi)存大小
  • _originalPreoptCachepreopt_cache_t *結(jié)構(gòu)體指針類型看疗,占8個字節(jié)
  • 結(jié)構(gòu)體中有_maybeMask,_flags,_occupied_maybeMaskmask_t類型睦授,mask_t又是uint32_t類型两芳,占4個字節(jié),_flags_occupieduint16_t類型去枷,占2個字節(jié)

綜上所述cache_t的內(nèi)存大小為16字節(jié)怖辆。

總結(jié):
  • isa內(nèi)存地址為首地址
  • superclass地址為首地址+0x08
  • cache_t地址為首地址+0x10
  • bits地址為首地址+0x20
    類的結(jié)構(gòu)圖.png

bits數(shù)據(jù)結(jié)構(gòu)

上面已經(jīng)了解的基本結(jié)構(gòu)是复,isasuperclass已經(jīng)探究過了,接下來我們先來研究一下成員變量bits存儲了哪些信息竖螃?

struct objc_class : objc_object {
  objc_class(const objc_class&) = delete;
  objc_class(objc_class&&) = delete;
  void operator=(const objc_class&) = delete;
  void operator=(objc_class&&) = delete;
    // 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 getSuperclass() const {...}
    void setSuperclass(Class newSuperclass) {...}

    class_rw_t *data() const {
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }

bitsclass_data_bits_t類型淑廊,底層源碼中還有一個data(),返回bits.data()特咆,這有可能就是bits中存儲的數(shù)據(jù)季惩。data()的類型是class_rw_t

struct class_rw_t {
    ... //省略一些沒用的方法
    const class_ro_t *ro() const {
        auto v = get_ro_or_rwe();
        if (slowpath(v.is<class_rw_ext_t *>())) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro;
        }
        return v.get<const class_ro_t *>(&ro_or_rw_ext);
    }

    void set_ro(const class_ro_t *ro) {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro = ro;
        } else {
            set_ro_or_rwe(ro);
        }
    }
    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};
        }
    }

class_rw_t是一個結(jié)構(gòu)體腻格,里面存儲著方法,屬性,協(xié)議列表蜀备,接下來驗證是否存儲在class_rw_t中。

屬性探究( properties() )
@interface JCPerson : NSObject{
    NSString *subject;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *hobby;

- (void)sayNB;
+ (void)say666;
@end
類結(jié)構(gòu)中屬性分析.png
  • property_list_t中存儲name,hobby屬性
  • p $7.get(2)會提示數(shù)組越界荒叶,沒有找到成員變量subject

問題:那么定義的subject成員變量存到哪里去了呢?

補(bǔ)充:成員變量

class_rw_t中除了有屬性,方法,協(xié)議以外输虱,還有class_ro_t結(jié)構(gòu)體指針類型的ro()些楣,在class_ro_t結(jié)構(gòu)體中我們可以找到ivar_list_t類型的指針ivars成員變量會不會存在這里呢宪睹?

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    union {
        const uint8_t * ivarLayout;
        Class nonMetaclass;
    };

    explicit_atomic<const char *> name;
    // With ptrauth, this is signed if it points to a small list, but
    // may be unsigned if it points to a big list.
    void *baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
類結(jié)構(gòu)中成員變量分析.png
源碼和LLDB分析:
  • 成員變量底層實現(xiàn)是ivar_t愁茁,存儲在class_ro_t成員變量列表
  • 系統(tǒng)是自動給屬性添加_屬性名的變量,存儲在class_ro_t成員變量列表
方法探究( methods() )

類結(jié)構(gòu)的方法分析.png

通過LLDB的方式亭病,可以得到定義的方法鹅很。但是發(fā)現(xiàn)使用get(index)的方式無法得到,?使用get(index).big()才能獲取罪帖,這是為什么呢促煮?

struct property_t {
    const char *name;
    const char *attributes;
};

struct method_t {
    struct big {
        SEL name;
        const char *types;
        MethodListIMP imp;
    };
    big &big() const {
        ASSERT(!isSmall());
        return *(struct big *)this;
    }

    SEL name() const {
        if (isSmall()) {
            return (small().inSharedCache()
                    ? (SEL)small().name.get()
                    : *(SEL *)small().name.get());
        } else {
            return big().name;
        }
    }
    const char *types() const {
        return isSmall() ? small().types.get() : big().types;
    }
    IMP imp(bool needsLock) const {
        if (isSmall()) {
            IMP imp = remappedImp(needsLock);
            if (!imp)
                imp = ptrauth_sign_unauthenticated(small().imp.get(),
                                                   ptrauth_key_function_pointer, 0);
            return imp;
        }
        return big().imp;
    }

源碼分析:
屬性和方法獲取區(qū)別.png
  • 屬性底層實現(xiàn)是property_t,在property_t結(jié)構(gòu)體中定義了name等變量
  • 方法底層實現(xiàn)是method_t整袁,在method_t結(jié)構(gòu)體中定義了一個big()菠齿,通過big()獲取SELIMP

接下來我們繼續(xù)打印methods,如下圖坐昙。

類結(jié)構(gòu)的方法分析2.png

  • method_list_t中有對象方法,屬性的setter方法getter方法
  • method_list_t中沒有獲取到類方法

問題:那么類方法存儲到哪里去了呢绳匀?

補(bǔ)充:類方法

對象方法存儲在中,那類方法可能存儲在元類炸客。

類方法的分析.png

  • object_getClass獲取到JCPerson的元類
  • 元類中method_list_t中存儲著類方法
總結(jié):
  • 類的結(jié)構(gòu)主要由isa,superclass,cache,bits組成
  • bits中存儲著屬性,方法,協(xié)議
  • 屬性存儲在property_list_t中疾棵,而成員變量存儲在class_ro_t-->ivar_list_t,系統(tǒng)為屬性自動生成的_屬性名的變量也存儲在class_ro_t-->ivar_list_t
  • 方法存儲在method_list_t中痹仙,method_list_t主要存儲著對象方法,屬性的setter方法getter方法是尔,而類方法存儲在元類中的method_list_t
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蝶溶,隨后出現(xiàn)的幾起案子嗜历,更是在濱河造成了極大的恐慌宣渗,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件梨州,死亡現(xiàn)場離奇詭異痕囱,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)暴匠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門鞍恢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人每窖,你說我怎么就攤上這事帮掉。” “怎么了窒典?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵蟆炊,是天一觀的道長。 經(jīng)常有香客問我瀑志,道長涩搓,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任劈猪,我火速辦了婚禮昧甘,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘战得。我一直安慰自己充边,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布常侦。 她就那樣靜靜地躺著浇冰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪刮吧。 梳的紋絲不亂的頭發(fā)上湖饱,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機(jī)與錄音杀捻,去河邊找鬼井厌。 笑死,一個胖子當(dāng)著我的面吹牛致讥,可吹牛的內(nèi)容都是我干的仅仆。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼垢袱,長吁一口氣:“原來是場噩夢啊……” “哼墓拜!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起请契,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤咳榜,失蹤者是張志新(化名)和其女友劉穎夏醉,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體涌韩,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡畔柔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了臣樱。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片靶擦。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖雇毫,靈堂內(nèi)的尸體忽然破棺而出玄捕,到底是詐尸還是另有隱情,我是刑警寧澤棚放,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布枚粘,位于F島的核電站,受9級特大地震影響飘蚯,放射性物質(zhì)發(fā)生泄漏赌结。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一孝冒、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧拟杉,春花似錦庄涡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至拿穴,卻和暖如春泣洞,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背默色。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工球凰, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人腿宰。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓呕诉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親吃度。 傳聞我的和親對象是個殘疾皇子甩挫,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

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