OC底層原理(四)、isa走向與class_data_bits_t

OC底層原理匯總

上一篇中,我們對isa的初始化、類與對象的底層結(jié)構(gòu)以及屬性進行了簡單剝析。

  • 對于isa,我們得出結(jié)論,isa是一個存儲了所屬類的地址;
  • 對于類的底層結(jié)構(gòu),我們得出了類有四個成員,isa、superclass、cache、bits
  • 對于屬性,我們得出了屬性是由實例變量+setter+getter組成。

在這一篇中,我們將進一步探析:

  • isa指向的類的isa指向什么呢,如果一直指向下去,最終會指向哪里?
  • 對于cache、bits,它們分別是用來做什么的呢?它們內(nèi)部結(jié)構(gòu)是什么樣的呢?

我們接下來逐一進行分析

一、isa指向

1.使用lldb命令分析isa指向

我們在main.m中,首先創(chuàng)建一個LWPerson類繼承自NSObject,然后再創(chuàng)建一個LWStudent繼承自LWPerson,并在main函數(shù)中創(chuàng)建兩個類的實例,在創(chuàng)建后打一個斷點,如下圖所示:

創(chuàng)建兩個類并添加斷點

上一篇中我們知道,isa.bits & ISA_MASK就可以獲得isa指向的類的地址,據(jù)此,我們使用lldb來調(diào)試查找isa的走位,如下面代碼所示

//16進制打印student的地址
(lldb) p/x student 
(LWStudent *) $0 = 0x0000000100546690
//根據(jù)student的內(nèi)容連續(xù)打印四段,第一段就是isa.bits
(lldb) x/4gx $0
0x100546690: 0x001d8001000023d5 0x00000000003c0001
0x1005466a0: 0x0000000100001030 0x0000000000000457
//得到isa指向的類的地址
(lldb) p/x 0x001d8001000023d5 & 0x00007ffffffffff8ULL
(unsigned long long) $1 = 0x00000001000023d0
//po之后,可以看到指向LWStudent
(lldb) po $1
LWStudent

//繼續(xù)找LWStudent的isa指向
(lldb) x/4gx $1
0x1000023d0: 0x00000001000023a8 0x0000000100002380
0x1000023e0: 0x00000001005466f0 0x0002802c00000007
(lldb) p/x 0x00000001000023a8 & 0x00007ffffffffff8ULL
(unsigned long long) $2 = 0x00000001000023a8
//po 之后發(fā)現(xiàn),也是指向LWStudent
(lldb) po $2
LWStudent

//繼續(xù)查找isa指向
(lldb) x/4gx $2
0x1000023a8: 0x00007fff991e60f0 0x0000000100002358
0x1000023b8: 0x0000000100546770 0x0003e03500000007
(lldb) p/x 0x00007fff991e60f0 & 0x00007ffffffffff8ULL
(unsigned long long) $3 = 0x00007fff991e60f0
//此處發(fā)現(xiàn)指向NSObject
(lldb) po $3
NSObject

//繼續(xù)查找NSObject的isa指向
(lldb) x/4gx $3
0x7fff991e60f0: 0x00007fff991e60f0 0x00007fff991e6118
0x7fff991e6100: 0x0000000100704fb0 0x0004e03100000007
(lldb) p/x 0x00007fff991e60f0 & 0x00007ffffffffff8ULL
(unsigned long long) $4 = 0x00007fff991e60f0
(lldb) po $4
//發(fā)現(xiàn),NSObject的isa還是指向NSObject
NSObject

//我們繼續(xù)查找
(lldb) x/4gx $4
0x7fff991e60f0: 0x00007fff991e60f0 0x00007fff991e6118
0x7fff991e6100: 0x0000000100704fb0 0x0004e03100000007
(lldb) p/x 0x00007fff991e60f0 & 0x00007ffffffffff8ULL
(unsigned long long) $5 = 0x00007fff991e60f0
(lldb) po $5
//發(fā)現(xiàn)仍然是NSObject,并且地址都是0x00007fff991e60f0
NSObject

上方的lldb指令大致流程就是從查找studentisa指向出發(fā),不斷的查找指向的指向,其大致流程圖如下
[圖片上傳失敗...(image-e690ab-1600595328194)]
從案例中我們可以發(fā)現(xiàn),對象studentisa指向Student,而Studentisa也是指向的Student,Studentisa指向NSObject,而NSObjectisa指向自己。
在這里,連續(xù)出現(xiàn)了兩個Student,這兩個的地址相差40個字節(jié),并不是同一個,為什么會出現(xiàn)兩個不同的Student呢?

為了分析這個問題,我們使用lldb命令,新創(chuàng)建一個NSObject類的對象,繼續(xù)查找是否會出現(xiàn)兩個NSObject,如下所示

//創(chuàng)建一個NSObject的對象
(lldb) expr [NSObject alloc]
(NSObject *) $6 = 0x000000010071b990
(lldb) po $6
<NSObject: 0x10071b990>

(lldb) x/4gx $6
0x10071b990: 0x001dffff991e6119 0x0000000000000000
0x10071b9a0: 0x656b6f54534e5b2d 0x7420646c6569466e
(lldb) p/x 0x001dffff991e6119 & 0x00007ffffffffff8ULL
//找到它的指向,地址為0x00007fff991e6118
(unsigned long long) $7 = 0x00007fff991e6118
(lldb) po $7
NSObject

//繼續(xù)查找NSObject的isa指向
(lldb) x/4gx $7
0x7fff991e6118: 0x00007fff991e60f0 0x0000000000000000
0x7fff991e6128: 0x0000000100630340 0x0004801000000007
(lldb) p/x 0x00007fff991e60f0 & 0x00007ffffffffff8ULL
//找到NSObject的isa指向,地址為0x00007fff991e60f0
(unsigned long long) $8 = 0x00007fff991e60f0
(lldb) po $8
NSObject

測試后可以看出,NSObject也有兩個,兩者的地址相差也是40個字節(jié),其最終指向的NSObject的地址為0x00007fff991e60f0,與我們之前指令得到的最終指向NSObject的地址相同。

2.通過調(diào)用runtime API來分析isa走向

我們在main函數(shù)中編輯代碼如下:

int main(int argc, const char * argv[]) {
    
    LWStudent *student = [LWStudent alloc];
    
    Class class1 = object_getClass(student);
    NSLog(@"class1 is %@",class1);
    
    Class class2 = object_getClass(class1);
    NSLog(@"class2 is %@",class2);
    
    Class class3 = object_getClass(class2);
    NSLog(@"class3 is %@",class3);
    
    Class class4 = object_getClass(class3);
    NSLog(@"class4 is %@",class4);
    
    Class class5 = object_getClass(class4);
    NSLog(@"class5 is %@",class5);
    
    return 0;
}
//打印結(jié)果
class1 is LWStudent
class2 is LWStudent
class3 is NSObject
class4 is NSObject
class5 is NSObject

結(jié)果與lldb調(diào)試得到的結(jié)果一致

3.isa指向圖、類對象、元類

綜合以上分析,我們得到了這張經(jīng)典圖


isa指向經(jīng)典圖

在之前的測試中,我們得到兩個類分別稱之為類對象與元類,類對象是元類的對象,而NSObject的元類我們稱之為根元類

這時,isa的指向我們可以總結(jié)為:對象->類對象->元類->根元類->根元類。根元類繼承于NSObject,它的isa指向自身。

4.分析類結(jié)構(gòu)體struct objc_class結(jié)構(gòu)

同時,根據(jù)我們之前的測試,類對象的地址比它的元類高40字節(jié),我們根據(jù)struct objc_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);
    }
}

其中cache_t是一個結(jié)構(gòu)體,它內(nèi)部所有的成員變量如下:

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;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
#else
#error Unknown cache mask storage type.
#endif   
    uint16_t _flags;
    uint16_t _occupied;
}

其中,uintptr_tunsigned long的別名,占8個字節(jié);mask_tuint_32_t的別名,占4個字節(jié)
另外,我們看到,cache_t內(nèi)部針對不同平臺使用了不同的存儲結(jié)構(gòu),我們可以在objc-runtime-new.h中看到對于各個存儲類型的定義

#define CACHE_MASK_STORAGE_OUTLINED 1
#define CACHE_MASK_STORAGE_HIGH_16 2
#define CACHE_MASK_STORAGE_LOW_4 3

#if defined(__arm64__) && __LP64__
//arm64架構(gòu)且是64位Unix系統(tǒng),也就是我們現(xiàn)在的iOS設(shè)備
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16
#elif defined(__arm64__) && !__LP64__
//arm64架構(gòu)但不是64位Unix系統(tǒng),iphone 5s之前的設(shè)備都是32位
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4
#else
//其它,MAC_OS設(shè)備
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED
#endif

回到我們的cache_t結(jié)構(gòu)體中,無論是哪種平臺,根據(jù)內(nèi)存對齊規(guī)則,它需要的存儲空間都是16字節(jié)

我們再看class_data_bits_t的結(jié)構(gòu),它內(nèi)部的結(jié)構(gòu)比較簡單

struct class_data_bits_t {
    uintptr_t bits;
}

所以,class_data_bits_t bits占8字節(jié)。

綜上所述,struct objc_class所占的內(nèi)存如下圖所示,正好是40個字節(jié)

類結(jié)構(gòu)字節(jié)占用圖

所以,我們得出結(jié)論,在內(nèi)存中,類對象元類的存儲是緊密挨在一起的。

二、class_data_bits_t

接下來我們分析class_data_bits_t,在objc781源碼中,class_data_bits_t和以前有了很大的改變,現(xiàn)在的源碼中,它只有一個成員bits

  • 想要獲取class_rw_t,可以通過struct objc_class的常成員函數(shù)class_rw_t* data()來獲取;或者通過地址偏移先獲得class_data_bits_t,再通過它的常成員函數(shù)class_rw_t* data()來獲取。

  • 想要獲取class_ro_t,可以通過struct class_data_bits_t的成員方法const class_ro_t *safe_ro()獲取;或者在獲取到class_rw_t的數(shù)據(jù)后,使用struct class_rw_t的常成員函數(shù)class_ro_t *ro()來獲取。

1.地址偏移

在測試class_data_bits_t之前,我們先介紹一下什么是地址偏移。

有如下C語言的代碼:

int a[] = {0,1,2,3,4,5,6,7,8,9};
int temp1 = a[4];
int temp2 = *(a+4);

我們知道,C語言中,數(shù)組名就是指向數(shù)組首地址的指針,我們在獲取數(shù)組元素的時候,可以使用下標獲取a[4],也可以通過指針偏移的方式*(a+4),因為a有被定義為了int類型,所以,加一個偏移量就是偏移一個int的字節(jié)大小,在上例中,a[4]*(a+4)獲取到的值相同,都是獲取數(shù)組第5個元素的值。

內(nèi)存中,我們要讀取內(nèi)存中的值也可以使用這樣的方式,由于地址是沒有類型的,所以我們的偏移量需要直接設(shè)置為多少字節(jié)。

如我們有了struct objc_class的指針,根據(jù)我們之前對它內(nèi)部結(jié)構(gòu)的分析,我們獲取它指向的內(nèi)容地址后,可以如下得到各個成員的地址:

  • 前面8個字節(jié)就是isa
  • 偏移8個字節(jié)可以得到父類指針superclass
  • 偏移16個字節(jié)可以得到緩存cache
  • 偏移32個字節(jié)可以得到class_data_bits_t類型的bits數(shù)據(jù)

下面我們根據(jù)這個思想使用lldb找到class_data_bits_t,并且分析它內(nèi)部的成員。

2.lldb分析class_data_bits_t

1).設(shè)置調(diào)試案例

我們打開可編譯的源碼,在main.m中添加代碼如下:

@interface LWPerson : NSObject{
    NSInteger a;
    NSString *b;
    CGFloat c;
}
//姓名
@property (nonatomic,copy) NSString *name;
//年齡
@property (nonatomic,assign) short age;
//性別
@property (nonatomic,assign) BOOL isMan;

@end

@implementation LWPerson

- (void)sayHello{
    NSLog(@"%s",__func__);
}
- (void)sayByeBye{
    NSLog(@"%s",__func__);
}
+ (void)eat{
    NSLog(@"%s",__func__);
}
+ (void)drink{
    NSLog(@"%s",__func__);
}

@end

int main(int argc, const char * argv[]) {
    
    LWPerson *person = [[LWPerson alloc]init];
    person.name = @"Jobs";
    person.age = 77;
    person.isMan = YES;
    
    [person sayHello];
    [person sayByeBye];
    [LWPerson eat];
    [LWPerson drink];
    
    return 0;
}
2).lldb調(diào)試查看class_rw_t結(jié)構(gòu)

我們在[LWPerson drink];前添加一個斷點進行調(diào)試。

//獲取person的類LWPerson的地址
(lldb) p/x person.class
(Class) $0 = 0x0000000100002368 LWPerson
//地址偏移32個字節(jié)獲取class_data_bits_t,16進制下就是加上20
(lldb) p (class_data_bits_t *)0x0000000100002388
(class_data_bits_t *) $1 = 0x0000000100002388
//調(diào)用class_data_bits_t的成員方法data()獲取到class_rw_t的指針
(lldb) p $1->data()
(class_rw_t *) $2 = 0x0000000100655680
(lldb) p *$2
//查看class_rw_t的內(nèi)容
(class_rw_t) $3 = {
  flags = 2148007936
  witness = 1
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = 4294975656
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
}
3).class_ro_t、class_rw_t

關(guān)于class_ro_tclass_rw_t的探索過程我們以后再去探究,關(guān)于這兩個結(jié)構(gòu)體,我們先大致解釋一下:

  • class_ro_t是編譯期生成的,它存儲了當前類在編譯期就已經(jīng)確定的屬性、方法以及協(xié)議,它里面是沒有分類中定義的方法協(xié)議的。
  • class_rw_t是在運行時生成的,它在realizeClass中生成,它包含了class_ro_t。它在_objc_init方法中關(guān)于dyld的回調(diào)的map_images中最終將分類的方法協(xié)議都插入到自己的方法列表、協(xié)議列表中。它不包含成員變量列表,因為成員變量列表是在編譯期就確定好的,它只保存在class_ro_t中。不過,class_rw_t中包含了一個指向class_ro_t的指針。

class_rw_t的類結(jié)構(gòu)中,有如下一些成員函數(shù):

struct class_rw_t {

public:
    //獲取class_ro_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;
        }
        return v.get<const class_ro_t *>();
    }
    //設(shè)置class_ro_t
    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 = 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 *>()->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};
        }
    }
    //獲取協(xié)議列表
    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};
        }
    }
};

根據(jù)這些暴露出來的方法,我們繼續(xù)使用lldb查看class_rw_t的內(nèi)容。

4).尋找實例方法

我們繼續(xù)進行lldb調(diào)試,先看看有哪些方法

//獲取方法列表
(lldb) p $3.methods()
(const method_array_t) $4 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x00000001000020f0
      arrayAndFlag = 4294975728
    }
  }
}
//讀取實際的方法列表(list)
(lldb) p $4.list
(method_list_t *const) $5 = 0x00000001000020f0
//讀取list內(nèi)部的結(jié)構(gòu)
(lldb) p *$5
(method_list_t) $6 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 9
    first = {
      name = "sayHello"
      types = 0x0000000100000ebb "v16@0:8"
      imp = 0x0000000100000b90 (LWObjc`-[LWPerson sayHello] at main.m:27)
    }
  }
}
//獲取第一個方法,看得出來是- (void)sayHello
(lldb) p $6.get(0)
(method_t) $7 = {
  name = "sayHello"
  types = 0x0000000100000ebb "v16@0:8"
  imp = 0x0000000100000b90 (LWObjc`-[LWPerson sayHello] at main.m:27)
}
//獲取第二個方法,看得出來是- (void)sayByeBye
(lldb) p $6.get(1)
(method_t) $8 = {
  name = "sayByeBye"
  types = 0x0000000100000ebb "v16@0:8"
  imp = 0x0000000100000bc0 (LWObjc`-[LWPerson sayByeBye] at main.m:30)
}
////獲取第三個方法,看得出來是isMan的getter方法
(lldb) p $6.get(2)
(method_t) $9 = {
  name = "isMan"
  types = 0x0000000100000efd "c16@0:8"
  imp = 0x0000000100000c90 (LWObjc`-[LWPerson isMan] at main.m:21)
}
//獲取第四個方法,看得出來是isMan的setter方法
(lldb) p $6.get(3)
(method_t) $10 = {
  name = "setIsMan:"
  types = 0x0000000100000f05 "v20@0:8c16"
  imp = 0x0000000100000cb0 (LWObjc`-[LWPerson setIsMan:] at main.m:21)
}
//獲取第五個方法,看得出來是c++析構(gòu)方法
(lldb) p $6.get(4)
(method_t) $11 = {
  name = ".cxx_destruct"
  types = 0x0000000100000ebb "v16@0:8"
  imp = 0x0000000100000cd0 (LWObjc`-[LWPerson .cxx_destruct] at main.m:25)
}
//獲取第六個方法,看得出來是name的getter方法
(lldb) p $6.get(5)
(method_t) $12 = {
  name = "name"
  types = 0x0000000100000ed7 "@16@0:8"
  imp = 0x0000000100000bf0 (LWObjc`-[LWPerson name] at main.m:17)
}
//獲取第七個方法,看得出來是name的setter方法
(lldb) p $6.get(6)
(method_t) $13 = {
  name = "setName:"
  types = 0x0000000100000edf "v24@0:8@16"
  imp = 0x0000000100000c20 (LWObjc`-[LWPerson setName:] at main.m:17)
}
//獲取第八個方法,看得出來是age的getter方法
(lldb) p $6.get(7)
(method_t) $14 = {
  name = "age"
  types = 0x0000000100000eea "s16@0:8"
  imp = 0x0000000100000c50 (LWObjc`-[LWPerson age] at main.m:19)
}
//獲取第九個方法,看得出來是age的setter方法
(lldb) p $6.get(8)
(method_t) $15 = {
  name = "setAge:"
  types = 0x0000000100000ef2 "v20@0:8s16"
  imp = 0x0000000100000c70 (LWObjc`-[LWPerson setAge:] at main.m:19)
}
//獲取第十個方法時提示越界
(lldb) p $6.get(9)
Assertion failed: (i < count), function get, file .../runtime/objc-runtime-new.h, line 438.
error: Execution was interrupted, reason: signal SIGABRT.
The process has been returned to the state before expression evaluation.

從結(jié)果我們可以看出,所有的實例方法和屬性的setter、getter方法都存放在類對象的class_rw_t的方法列表中

方法types v16@0:8的解釋:
v表示方法返回值是void,16表示方法所有參數(shù)一共占16字節(jié),由于所有OC方法有兩個默認的參數(shù)id selfSEL _cmd,@表示對象類型,也就是id self,0表示這個參數(shù)從0字節(jié)位置開始,:表示方法,也就是SEL _cmd,8表示這個參數(shù)從第8個字節(jié)位置開始

5).探索屬性列表

我們再看看屬性列表

//獲取屬性列表
(lldb) p $3.properties()
(const property_array_t) $16 = {
  list_array_tt<property_t, property_list_t> = {
     = {
      list = 0x0000000100002298
      arrayAndFlag = 4294976152
    }
  }
}
//讀取實際的屬性列表
(lldb) p $16.list
(property_list_t *const) $17 = 0x0000000100002298
//我們在讀取屬性列表的時候被打斷了,沒有權(quán)限
(lldb) p *$17
error: Couldn't apply expression side effects : Couldn't dematerialize a result variable: couldn't read its memory
6).探索成員變量列表

在前面的我們提到,成員變量在class_ro_t中,而不在class_rw_t中,以為編譯結(jié)束類的結(jié)構(gòu)就已經(jīng)確定了,不能夠再被改變。

所以,我們先從class_rw_t獲取到class_ro_t,再查找ivars

//得到`class_ro_t`
(lldb) p $3.ro()
(const class_ro_t *) $19 = 0x00000001000020a8
//查看`class_ro_t`結(jié)構(gòu)
(lldb) p *$19
(const class_ro_t) $20 = {
  flags = 388
  instanceStart = 8
  instanceSize = 48
  reserved = 0
  ivarLayout = 0x0000000100000e4b "\x11!"
  name = 0x0000000100000e42 "LWPerson"
  baseMethodList = 0x00000001000020f0
  baseProtocols = 0x0000000000000000
  ivars = 0x00000001000021d0
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x0000000100002298
  _swiftMetadataInitializer_NEVER_USE = {}
}
//獲得ivars
(lldb) p $20.ivars
(const ivar_list_t *const) $21 = 0x00000001000021d0
//查看ivars
(lldb) p *$21
(const ivar_list_t) $22 = {
  entsize_list_tt<ivar_t, ivar_list_t, 0> = {
    entsizeAndFlags = 32
    count = 6
    first = {
      offset = 0x0000000100002310
      name = 0x0000000100000e58 "a"
      type = 0x0000000100000ec3 "q"
      alignment_raw = 3
      size = 8
    }
  }
}
//獲取第一個成員變量,這個是a
(lldb) p $22.get(0)
(ivar_t) $23 = {
  offset = 0x0000000100002310
  name = 0x0000000100000e58 "a"
  type = 0x0000000100000ec3 "q"
  alignment_raw = 3
  size = 8
}
//獲取第二個成員變量,這個是b
(lldb) p $22.get(1)
(ivar_t) $24 = {
  offset = 0x0000000100002318
  name = 0x0000000100000e5a "b"
  type = 0x0000000100000ec5 "@\"NSString\""
  alignment_raw = 3
  size = 8
}
//獲取第三個成員變量,這個是c
(lldb) p $22.get(2)
(ivar_t) $25 = {
  offset = 0x0000000100002320
  name = 0x0000000100000e5c "c"
  type = 0x0000000100000ed1 "d"
  alignment_raw = 3
  size = 8
}
//獲取第四個成員變量,這個是_isMan
(lldb) p $22.get(3)
(ivar_t) $26 = {
  offset = 0x0000000100002328
  name = 0x0000000100000e5e "_isMan"
  type = 0x0000000100000ed3 "c"
  alignment_raw = 0
  size = 1
}
//獲取第五個成員變量,這個是_age
(lldb) p $22.get(4)
(ivar_t) $27 = {
  offset = 0x0000000100002330
  name = 0x0000000100000e65 "_age"
  type = 0x0000000100000ed5 "s"
  alignment_raw = 1
  size = 2
}
//獲取第六個成員變量,這個是_name
(lldb) p $22.get(5)
(ivar_t) $28 = {
  offset = 0x0000000100002338
  name = 0x0000000100000e6a "_name"
  type = 0x0000000100000ec5 "@\"NSString\""
  alignment_raw = 3
  size = 8
}
//獲取第七個報錯,提示我們越界了
(lldb) p $22.get(6)
Assertion failed: (i < count), function get, file .../runtime/objc-runtime-new.h, line 438.
error: Execution was interrupted, reason: signal SIGABRT.
The process has been returned to the state before expression evaluation.

3.類方法存儲位置的探索

在上面的案例探索中,我們都沒有發(fā)現(xiàn)類方法+ (void)eat+ (void)drink,這是為什么呢?

第一部分中,我們談了isa指向的問題,在我們的方法查找的過程中,我們的實例方法存放在類對象中,這部分我們已經(jīng)驗證了。而類方法保存在元類方法列表中。

我們接下來對此進行驗證

//得到LWPerson的元類
(lldb) p/x object_getClass(person.class)
(Class) $0 = 0x0000000100002340
(lldb) po $0
LWPerson
//加上32字節(jié),16進制下就是加20,得到class_data_bits_t
(lldb) p (class_data_bits_t *)0x0000000100002360
(class_data_bits_t *) $1 = 0x0000000100002360
//獲取class_rw_t
(lldb) p $1->data()
(class_rw_t *) $2 = 0x000000010111a7c0
//查看class_rw_t結(jié)構(gòu)
(lldb) p *$2
(class_rw_t) $3 = {
  flags = 2684878849
  witness = 1
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = 4294975528
  }
  firstSubclass = nil
  nextSiblingClass = 0x00007fff8b25dcd8
}
//獲取方法列表
(lldb) p $3.methods()
(const method_array_t) $4 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x0000000100002070
      arrayAndFlag = 4294975600
    }
  }
}
//獲取實例的方法列表list
(lldb) p $4.list
(method_list_t *const) $5 = 0x0000000100002070
(lldb) p *$5
(method_list_t) $6 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 2
    first = {
      name = "eat"
      types = 0x0000000100000ebb "v16@0:8"
      imp = 0x0000000100000b30 (KCObjc`+[LWPerson eat] at main.m:33)
    }
  }
}
//獲取第一個方法,我們發(fā)現(xiàn)是類方法eat
(lldb) p $6.get(0)
(method_t) $7 = {
  name = "eat"
  types = 0x0000000100000ebb "v16@0:8"
  imp = 0x0000000100000b30 (KCObjc`+[LWPerson eat] at main.m:33)
}
//獲取第二個方法,我們發(fā)現(xiàn)是類方法drink
(lldb) p $6.get(1)
(method_t) $8 = {
  name = "drink"
  types = 0x0000000100000ebb "v16@0:8"
  imp = 0x0000000100000b60 (KCObjc`+[LWPerson drink] at main.m:36)
}
//獲取第三個方法時提示越界了
(lldb) p $6.get(2)
Assertion failed: (i < count), function get, file .../runtime/objc-runtime-new.h, line 438.
error: Execution was interrupted, reason: signal SIGABRT.
The process has been returned to the state before expression evaluation.

這就是驗證了類方法存放在元類方法列表。

三、總結(jié)

在本篇中,我們研究了isa的走向以及對class_data_bits_t的結(jié)構(gòu)進行了分析。

  • isa的走向為:對象->類對象->元類->根元類->根元類根元類繼承于NSObject,它的isa指向自身。

  • class_data_bits_t中只有一個bits,它通過算法可以得到class_rw_t,class_rw_t存儲了了類的方法列表、屬性列表、協(xié)議列表;而成員變量存放在class_ro_t中。實例方法存放在類對象方法列表中,類方法存放在元類方法列表中。

下一篇我們繼續(xù)分析關(guān)于類結(jié)構(gòu)中的cache_t的作用于內(nèi)部結(jié)構(gòu)與算法。

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

友情鏈接更多精彩內(nèi)容