《跟我學》之OC類的結構分析

我們之前的篇幅介紹了對象,也知道對象是一個實例。那么它的結構又是怎么樣的。為了更直接的觀察。我們做好充足的前戲提前定義好了兩個類。
Person繼承NSObject,Developer繼承Person ,代碼如下。

準備工作

Person.h文件

@interface Person : NSObject
{
    ///成員變量
     NSString *_variables;
}
///一個屬性
@property (nonatomic, strong) NSString *attributes;
///類方法
+ (void)classMethod;
///實例方法
- (void)instanceMethod;
@end

Developer.h文件

@interface Developer : Person
{
    ///成員變量
     NSString *_subVariables;
}
///一個屬性
@property (nonatomic, strong) NSString *subAttributes;
///類方法
+ (void)subClassMethod;
///實例方法
- (void)subInstanceMethod;
@end

main.m文件

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //ISA_MASK  0x00007ffffffffff8ULL
        Person *person = [Person alloc];
        Developer *developer = [Developer alloc];
        NSLog(@"person %@", person);
        NSLog(@"developer %@", developer);
    }
    return 0;
}

要用到的lldb指令

指令 作用
p expr - 的縮寫。它的工作是把接收到的參數(shù)在當前環(huán)境下進行編譯,然后打印出對應的值。
po expr -o-。它所做的操作與p相同。如果接收到的參數(shù)是一個指針,那么它會調用對象的 description 方法并打印。如果接收到的參數(shù)是一個 core foundation 對象,那么它會調用 CFShow 方法并打印。如果這兩個方法都調用失敗,那么 po 打印出和 p 相同的內容??偟膩碚f,po 相對于 p 會打印出更多內容。一般在工作中,用 p 即可,因為 p 操作較少,效率更高。
p/x 16進制讀取對象的地址或者值
x/4gx 16進制形式讀取48位的內存空間里面存儲的值
(lldb) x/4gx person
0x1039b9230: 0x001d80010000231d 0x0000000000000000
0x1039b9240: 0x0000000000000000 0x0000000000000000
(lldb) p/x 0x001d80010000231d & 0x00007ffffffffff8ULL
(unsigned long long) $4 = 0x0000000100002318
(lldb) po 0x0000000100002318
Person

(lldb) x/4gx Person.class
0x100002318: 0x00000001000022f0 0x00007fff91427118
0x100002328: 0x0000000100504630 0x000580240000000f
(lldb) p/x 0x00000001000022f0 & 0x00007ffffffffff8ULL
(unsigned long long) $7 = 0x00000001000022f0
(lldb) po 0x00000001000022f0
Person

(lldb) x/4gx 0x00000001000022f0
0x1000022f0: 0x00007fff914270f0 0x00007fff914270f0
0x100002300: 0x0000000100604270 0x0004e03500000007
(lldb) p/x 0x00007fff914270f0 & 0x00007ffffffffff8ULL
(unsigned long long) $9 = 0x00007fff914270f0
(lldb) po 0x00007fff914270f0
NSObject

(lldb) x/4gx 0x00007fff914270f0
0x7fff914270f0: 0x00007fff914270f0 0x00007fff91427118
0x7fff91427100: 0x00000001039b9880 0x0004e03100000007
(lldb) p/x 0x00007fff914270f0 & 0x00007ffffffffff8ULL
(unsigned long long) $11 = 0x00007fff914270f0
(lldb) po 0x00007fff914270f0
NSObject

我們打個斷點通過lldb來觀察一下person對象發(fā)現(xiàn)它的isa指針指向Person類,Person類的isa指針指向的還是Person類,
這里面有個細節(jié),就是盡管都是Person類,但是他們并不是一個,仔細觀察我們發(fā)現(xiàn)personisa指針地址為0x0000000100002318,而Person.classisa指針地址為0x00000001000022f0。他們并不是同一個類
我們把后面這個看起來像是它自己的稱之為元類。

隨后元類isa指針指向為NSObject的地址為0x00007fff914270f0。即便我們一直這樣觀察下去也發(fā)現(xiàn),循環(huán)指向0x00007fff914270f0NSObject。
再暴力點直接x/4gx NSObject.class發(fā)現(xiàn)它的isa也是0x00007fff914270f0

結論

1、對象isa 指向 (也可稱為類對象
person -> isa 他所屬的類 Person
2、isa 指向 元類
Person類 -> isa 他的Person元類 (雖然看起來是他自己,但真的不是它自己。因為isa地址不一樣)
3、元類isa 指向 根元類,即NSObject
Person元類 -> isa = 他的根元類NSObject
4、根元類isa 指向 它自己 也是NSObject
NSObject根源類 -> isa自己 NSObject(這個的內存地址始終唯一

我們來解釋一下什么是元類,應該就可以明白剛才所說的看起來是它自己。但是又不是它自己
我們都知道 對象isa 是指向的其實也是一個對象,可以稱為類對象,其isa的位域指向蘋果定義的元類
1、元類是系統(tǒng)給的,其定義和創(chuàng)建都是由編譯器完成,在這個過程中,的歸屬來自于元類
2、元類類對象,每個都有一個獨一無二元類用來存儲 類方法的相關信息。
3、元類本身是沒有名稱的,由于與相關聯(lián),所以使用了同類名一樣的名稱
如果簡單的理解話。你可以把它理解成一個副本類。有這一樣的名字

類是否唯一?

Class developerClass1 = [Developer class];
Class developerClass2 = [Developer alloc].class;
Class developerClass3 = object_getClass([Developer alloc]);
NSLog(@"developerClass1=%p", developerClass1);
NSLog(@"developerClass2=%p", developerClass2);
NSLog(@"developerClass3=%p", developerClass3);
/// developerClass1=0x100003380 developerClass2=0x100003380 developerClass3=0x100003380

Class personClass1 = [Person class];
Class personClass2 = [Person alloc].class;
Class personClass3 = object_getClass([Person alloc]);
NSLog(@"personClass1=%p", personClass1);
NSLog(@"personClass2=%p", personClass2);
NSLog(@"personClass3=%p", personClass3);
/// personClass1=0x100003330 personClass2=0x100003330 personClass3=0x100003330

Class objectClass1 = [NSObject class];
Class objectClass2 = [NSObject alloc].class;
Class objectClass3 = object_getClass([NSObject alloc]);
NSLog(@"personClass1=%p", objectClass1);
NSLog(@"personClass2=%p", objectClass2);
NSLog(@"personClass3=%p", objectClass3);
/// developerClass1=personClass1 personClass2=0x7fff91427118 personClass3=0x7fff91427118

我們又通過這一坨很無聊的代碼得出另外一個結論,任何內存只存在一份

核心isa走位 與 類的繼承關系圖

這個圖第一次我看很懵逼。現(xiàn)在看依然懵逼。
但是。學以致用,把他套入到我們自己的類用。就容易理解很多


手殘黨畫的不好。但是確實套用我們剛才示例代碼的進來。清晰了不少。
我們梳理一下這幅圖中的倆條線索

isa走位鏈路

1、實例對象 isa 指向他所屬的類
2、類對象isa指向它的元類(其實也是它,但是內存地址不是同一個)
3、元類的isa指向它的根元類
4、根元類的isa指向它,無限循環(huán),形成閉環(huán),所有的根元類就是NSObject

supclass走位鏈路

我們都知道類與類之間可以存在繼承關系
1、子類繼承父類
2、父類繼承根類,此時的根類是指NSObject
3、根類繼承nil,所以萬物皆NSObject。

元類也有繼承關系
1、子元類繼承父元類
2、父元類繼承根元類
3、根元類繼承根類,此時根類NSObject

容易混淆的一個點是。類與類之間是有繼承關系,但是實例對象實例對象之間沒有繼承關系
Developer,Person,NSObject三個的關系是Developer繼承Person,Person繼承NSObject
它們的實例對戲那個為developer, person, object。你不們理解為他們的類是繼承關系。所以他們三個實例對象也存在繼承關系。這是錯誤的。

objc_class & objc_object

isa走位我們理清楚了,又來了一個新的問題:為什么 對象都有isa屬性呢?
不提到兩個結構體類型:objc_class & objc_object
我們之前提及NSObject的底層編譯是NSObject_IMPL結構體

struct NSObject_IMPL {
    Class isa;
};
typedef struct objc_class *Class;

objc4源碼中搜索objc_class的定義,源碼中對其的定義有兩個版本

  • 舊版 位于 runtime.h中,已經被廢除,代碼如下
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;
  • 新版 位于objc-runtime-new.h,這個是objc4-781最新優(yō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);
    }
}

從新版的定義中,可以看到 objc_class 結構體類型是繼承自 objc_object的.objc_object定義又如下代碼

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

我們來探索一下類里面都哪些信息

isa屬性:繼承自objc_objectisa,占 8字節(jié)
superclass 屬性:Class類型,Class是由objc_object定義的,是一個指針,占8字節(jié)
cache屬性:簡單從類型class_data_bits_t目前無法得知,而class_data_bits_t是一個結構體類型,結構體的內存大小需要根據(jù)內部的屬性來確定,而結構體指針才是8字節(jié)
bits屬性:只有首地址經過上面3個屬性的內存大小總和的平移,才能獲取到bits

計算 cache 類的內存大小

進入cachecache_t的定義(只貼出了結構體中非static修飾的屬性,主要是因為static類型的屬性 不存在結構體的內存中),代碼如下

struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    explicit_atomic<struct bucket_t *> _buckets;
    explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;

計算前兩個屬性的內存大小,有以下兩種情況,最后的內存大小總和都是12字節(jié)

  • 【情況一】if流程
buckets 類型是struct bucket_t *,是結構體指針類型,占8字節(jié)
mask 是mask_t 類型,而 mask_t 是 unsigned int 的別名,占4字節(jié)
  • 【情況二】elseif流程
_maskAndBuckets 是uintptr_t類型,它是一個指針,占8字節(jié)
_mask_unused 是mask_t 類型,而 mask_t 是 uint32_t 類型定義的別名,占4字節(jié)

_flagsuint16_t類型,uint16_tunsigned short 的別名,占 2個字節(jié)
_occupieduint16_t類型,uint16_tunsigned short 的別名,占 2個字節(jié)

總結:所以最后計算出cache類的內存大小 = 12 + 2 + 2 = 16字節(jié)

接下來就是如何獲取bits了

要獲取bits的中的內容,只需通過類的首地址平移32字節(jié)即可
我們剛才發(fā)現(xiàn)。其中的data()獲取數(shù)據(jù),我們先利用lldb再次打印看能否觀察出有價值的信息


x/6gx Person.class 打印出類的首地址
p (class_data_bits_t *)0x100002568 打印出平移了32位的bits信息
最后我們打印發(fā)現(xiàn)了一個 class_rw_t.我們查看源代碼發(fā)現(xiàn)

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 *>()->methods;
        } else {
            return method_array_t{v.get<const class_ro_t *>()->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 *>()->properties;
        } else {
            return property_array_t{v.get<const class_ro_t *>()->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 *>()->protocols;
        } else {
            return protocol_array_t{v.get<const class_ro_t *>()->baseProtocols};
        }
    }

這三個是不是對應的方法,屬性,協(xié)議呢?lldb調試日志如下

p $3.methods()打印出方法數(shù)組
p $4.list獲取元類方法數(shù)組的方法列表
p *$5 獲取元類方法列表的第一個方法


我們通過 p $6.get(0) ,p $6.get(1)p $6.get(2), p $6.get(3)依次打印出來instanceMethod,cxx_destruct方法和兩個屬性方法attributessetAttributes。

通過上述內容最終得出

類的實例方法存儲在類的bits屬性中,例如Person類的實例方法instanceMethod 就存儲在 Person類的bits屬性中

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

友情鏈接更多精彩內容