
我們之前的篇幅介紹了對象,也知道對象是一個類的實例。那么它的結構又是怎么樣的。為了更直接的觀察。我們做好充足的前戲提前定義好了兩個類。
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進制形式讀取4個8位的內存空間里面存儲的值 |
(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)person的isa指針地址為0x0000000100002318,而Person.class的isa指針地址為0x00000001000022f0。他們并不是同一個類
我們把后面這個看起來像是它自己的類稱之為元類。
隨后元類的isa指針指向為NSObject的地址為0x00007fff914270f0。即便我們一直這樣觀察下去也發(fā)現(xiàn),循環(huán)指向0x00007fff914270f0即NSObject。
再暴力點直接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_object的isa,占 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 類的內存大小
進入cache類cache_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é)
_flags 是uint16_t類型,uint16_t是 unsigned short 的別名,占 2個字節(jié)
_occupied 是uint16_t類型,uint16_t是 unsigned 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方法和兩個屬性方法attributes,setAttributes。
通過上述內容最終得出
類的實例方法存儲在類的bits屬性中,例如Person類的實例方法instanceMethod 就存儲在 Person類的bits屬性中