類的分析
- 準備工作霜大,我們先創(chuàng)建兩個類繼承
NSObject
的LGPerson
和繼承LGPerson
的LGStudent
:
//.h文件
@interface LGPerson : NSObject
{
NSString *hobby;
}
@property (nonatomic, copy) NSString *lg_name;
- (void)sayHello;
+ (void)sayBye;
@end
//.m文件
@implementation LGPerson
- (void)sayHello
{
}
+ (void)sayBye
{
}
@end
- 在
main.m
文件中如下設(shè)置
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LGPerson *person = [LGPerson alloc];
LGStudent *student = [LGStudent alloc];
NSLog(@"Hello, World! %@ - %@",person,student);
}
return 0;
}
-
類結(jié)構(gòu)的分析
在mian.m
文件LGStudent *student = [LGStudent alloc];
處打上斷點運行一下項目工程,使用lldb
進行調(diào)試
lldb調(diào)試過程及輸出結(jié)果- 首先我們可以根據(jù)lldb命令得到
person的內(nèi)存分布
,我們知道0x001d8001000022dd
是person
的內(nèi)存指針妄迁,接著使用這個指針&0x00007ffffffffff8ULL
尿招,可以獲取類的相關(guān)信息$3 = 0x00000001000022d8
溜腐。 - 接著
po 0x00000001000022d8
來打印類的信息,我們發(fā)現(xiàn)結(jié)果為LGPerson
钱豁; -
x/4gx 0x00000001000022d8
,來打印LGPerson
的內(nèi)存情況,我們同樣的可以拿到類的isa指針地址0x00000001000022b0
; - 下一步吉执,
p/x 0x00000001000022b0 & 0x00007ffffffffff8ULL
,我們來獲取類的信息疯淫,我們得到$5 = 0x00000001000022b0
類的指針地址; - 下一步:通過
po 0x00000001000022b0
戳玫,發(fā)現(xiàn)結(jié)果還是LGPerson
熙掺,這是為什么呢?這里先簡單說一下這個LGPerson
是元類咕宿;下面小節(jié)在詳細說明元類的問題币绩。
- 首先我們可以根據(jù)lldb命令得到
二蜡秽、元類,主要有以下幾點說明:
我們都知道
對象的isa
是指向類
缆镣,類
其實也是一個對象
芽突,可以稱為類對象
,其isa的位域
指向蘋果定義的元類
董瞻。元類
是系統(tǒng)
設(shè)置的诉瓦,其定義和創(chuàng)建都是由編譯器
完成,在這個過程中力细,類的歸屬
來自于`元類。元類
是類對象
的類固额,每個類都有一個獨一無二的元類用來存儲 類方法的相關(guān)信息眠蚂。元類
本身是沒有名稱
的,由于與類相關(guān)聯(lián)
斗躏,所以使用了同類名一樣
的名稱逝慧。
總結(jié):由上圖的打印結(jié)果我們可以得出如下結(jié)論:對象 --> 類 --> 元類 --> NSobject, NSObject 指向自身
。
三啄糙、isa走位 & 繼承關(guān)系
根據(jù)上面的探索以及各種驗證笛臣,對象、類隧饼、元類沈堡、根元類
的關(guān)系如下圖所示
isa的走向有以下幾點說明:
-
實例對象(Instance of Subclass)
的isa
指向類(class)
-
類對象(class) isa
指向元類(Meta class)
-
元類(Meta class)的isa
指向根元類(Root metal class)
-
根元類(Root metal class) 的isa
指向它自己本身
,形成閉環(huán)燕雁,這里的根元類就是NSObject诞丽。
superclass(即繼承關(guān)系)的走向也有以下幾點說明:
-
類(subClass)
繼承自父類(superClass)
-
父類(superClass)
繼承自根類(RootClass)
,此時的根類是指NSObject拐格。 -
根類 繼承自 nil
僧免,所以根類即NSObject可以理解為萬物起源,即無中生有捏浊。 -
子類的元類(metal SubClass)
繼承自父類的元類(metal SuperClass)
懂衩。 -
父類的元類(metal SuperClass)
繼承自根元類(Root metal Class
。 -
根元類(Root metal Class)
繼承于根類(Root class)
金踪,此時的根類是指NSObject浊洞。
舉例說明
isa 走位鏈
-
student
的isa
走位鏈:student(子類對象) --> LGStudent (子類)--> LGStudent(子元類) --> NSObject(根元類) --> NSObject(跟根元類,即自己)
-person
的isa
走位圖:person(父類對象) --> CJLPerson (父類)--> CJLPerson(父元類) --> NSObject(根元類) --> NSObject(跟根元類热康,即自己)
superclass走位鏈
- 類的繼承關(guān)系鏈:
LGStudent(子類) --> CJLPerson(父類) --> NSObject(根類)--> nil
- 元類的繼承關(guān)系鏈:
LGStudent(子元類) --> CJLPerson(父元類) --> NSObject(根元類)--> NSObject(根類)--> nil
四沛申、objc_class & objc_object
在分析objc_class & objc_object
我們先引入一個問題,為什么類和對象都有isa屬性
呢姐军?
我們先將main.m文件
編譯為main.cpp
來分析一下這個問題铁材。我們根據(jù)clang編譯的c++源碼
可以看出NSObject的底層編譯
是NSObject_IMPL結(jié)構(gòu)體
尖淘,并且對象含有Class isa
,代碼如下
struct NSObject_IMPL {
Class isa;
};
typedef struct objc_class *Class;
struct objc_object {
Class _Nonnull isa __attribute__((deprecated));
};
下面我們通過在objc4源碼
中搜索來探索objc_class & objc_object
-
objc_class
在源碼中搜索到兩種相關(guān)源碼 - 第一種已經(jīng)不再使用了著觉,并且和我們使用
Clang編譯
后的一樣
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
} OBJC2_UNAVAILABLE;
另外一種村生,我們選擇主要的代碼進行展示如下
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();
}
};
我們發(fā)現(xiàn)objc_class
繼承自objc_object
,下面我們再看看objc_object源碼
饼丘,
-
objc_object
源碼
struct objc_object {
private:
isa_t isa;
};
總結(jié):
- 我們根據(jù)源碼發(fā)現(xiàn)
objc_object結(jié)構(gòu)體
定義了isa
作為它的一個屬性
趁桃,objc_class
繼承自objc_object
,所以objc_class
也擁有了isa屬性肄鸽。 -
mian.cpp
底層編譯文件中卫病,NSObject中的isa
在底層是由Class 定義
的,其中class的底層編碼
來自objc_class類型
典徘,所以NSObject
也擁有了isa屬性
蟀苛。 -
NSObject
是一個類,用它初始化一個實例對象objc
逮诲,objc
滿足objc_object
的特性帜平,所以對象
都有一個 isa
,isa
表示指向梅鹦,來自于當(dāng)前的objc_object
裆甩。 - 所以所有的對象都是以
objc_object為模板繼承過來
的。 - 因為
對象
是 來自NSObject(OC)
齐唆,但是真正到底層的 是一個objc_object(C/C++)的結(jié)構(gòu)體類型
,所以objc_object
與對象的關(guān)系
是繼承關(guān)系
嗤栓。
objc_class
、objc_object
箍邮、isa
抛腕、object
、NSObject
等的整體的關(guān)系媒殉,如下圖所示
類的方法
我們根據(jù)下面類的底層實現(xiàn)源碼來探索一下担敌,類的實例方法存儲在哪里,來具體探索一下類的結(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;
// ......部分代碼不在展示
}
- 在探索之前我們需要補充一下知識全封,關(guān)于
內(nèi)存偏移
,我們先使用實例說明一下這個內(nèi)存偏移
桃犬;
【普通指針】
//普通指針
int a = 10; //變量
int b = 10;
NSLog(@"%d -- %p", a, &a);
NSLog(@"%d -- %p", b, &b);
輸出的結(jié)果我們可以從控制臺看出刹悴,a和b兩個指針指向了同一片的存儲著10的空間。
a攒暇、b
都指向10
土匀,但是a、b的地址``不一樣形用,這是一種拷貝
就轧,屬于值拷貝证杭,也稱為淺拷貝
a,b的地址
之間相差4
個字節(jié),這取決于a妒御、b的類型
【對象指針】
LGPerson *p1 = [LGPerson alloc]; // p1 是指針
LGPerson *p2 = [LGPerson alloc];
NSLog(@"%d -- %p", p1, &p1);
NSLog(@"%d -- %p", p2, &p2);
輸出結(jié)果
p1解愤、p2
是指針,p1
是 指向[LGPerson alloc]創(chuàng)建的空間地址
乎莉,即內(nèi)存地址送讲,p2
同理&p1、&p2
是 指向p1惋啃、p2對象指針的地址
哼鬓,這個指針 就是 二級指針
【數(shù)組指針】
//數(shù)組指針
int c[4] = {1, 2, 3, 4};
int *d = c;
NSLog(@"%p -- %p - %p", &c, &c[0], &c[1]);
NSLog(@"%p -- %p - %p", d, d+1, d+2);
輸出結(jié)果-
&c
和&c[0]
都是取這個數(shù)組的首地址
,所以``數(shù)組名等同于首地址
边灭; -
&c
與&c[1]
相差4個字節(jié)
魄宏,地址之間相差的字節(jié)數(shù)
,主要取決于存儲的數(shù)據(jù)類型可以通過
首地址+偏移量取出數(shù)組中的其他元素存筏,其中
偏移量是
數(shù)組的下標,
內(nèi)存中首地址實際移動的字節(jié)數(shù)等于
偏移量 * 數(shù)據(jù)類型字節(jié)數(shù)`味榛;
計算類結(jié)構(gòu)的內(nèi)存大小
通過上面類結(jié)構(gòu)的源碼我們來計算一下類的大小
-isa屬性
:繼承自objc_object
的isa
椭坚,占 8
字節(jié);
-
superclass
屬性:Class
類型搏色,Class
是由objc_object
定義的善茎,是一個指針
,占8
字節(jié) -
cache
屬性:是cache_t
結(jié)構(gòu)體類型频轿,我們應(yīng)該按照計算結(jié)構(gòu)體內(nèi)存大小的規(guī)則來計算垂涯,而結(jié)構(gòu)體指針
才是8字節(jié)
;下面代碼可以計算出cache占16字節(jié)
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
explicit_atomic<struct bucket_t *> _buckets;// 是一個結(jié)構(gòu)體指針類型航邢,占8字節(jié)
explicit_atomic<mask_t> _mask;//是mask_t 類型耕赘,而 mask_t 是 unsigned int 的別名,占4字節(jié)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic<uintptr_t> _maskAndBuckets;
mask_t _mask_unused;
//省略部分代碼
#if __LP64__
uint16_t _flags; //是uint16_t類型膳殷,uint16_t是 unsigned short 的別名操骡,占 2個字節(jié)
#endif
uint16_t _occupied; //是uint16_t類型,uint16_t是 unsigned short 的別名赚窃,占 2個字節(jié)
//省略部分代碼
}
-
bits屬性:只有首地址經(jīng)過上面
3個屬性的內(nèi)存大小總和的平移册招,才能獲取到
bits`;
類結(jié)構(gòu)中方法分析流程
(lldb) p/x LGPerson.class
(Class) $0 = 0x00000001000022f0 LGPerson
(lldb) //平移32個字節(jié)
error: '//平移32個字節(jié)' is not a valid command.
(lldb) p/x 0x00000001000022f0 + 32
(long) $1 = 0x0000000100002310
(lldb) 獲取bit
error: '獲取bit' is not a valid command.
(lldb) p (class_data_bits_t *)0x0000000100002310
(class_data_bits_t *) $2 = 0x0000000100002310
(lldb) 通過結(jié)構(gòu)體的data()來獲取bits
error: '通過結(jié)構(gòu)體的data()來獲取bits' is not a valid command.
(lldb) p $2 -> data()
(class_rw_t *) $3 = 0x0000000102018a20
(lldb) 獲取methods()
error: '獲取methods()' is not a valid command.
(lldb) p $3.methods()
(const method_array_t) $4 = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x0000000100002180
arrayAndFlag = 4294975872
}
}
}
Fix-it applied, fixed expression was:
$3->methods()
(lldb) 獲取list
error: '獲取list' is not a valid command.
(lldb) p $4.list
(method_list_t *const) $5 = 0x0000000100002180
(lldb) 打印list內(nèi)容
error: '打印list內(nèi)容' is not a valid command.
(lldb) p *$5
(method_list_t) $6 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 4
first = {
name = "sayHello"
types = 0x0000000100000f85 "v16@0:8"
imp = 0x0000000100000d80 (KCObjc`-[LGPerson sayHello])
}
}
}
(lldb)
- 關(guān)于類的屬性
(lldb) p $3.properties()
(const property_array_t) $7 = {
list_array_tt<property_t, property_list_t> = {
= {
list = 0x0000000100002230
arrayAndFlag = 4294976048
}
}
}
Fix-it applied, fixed expression was:
$3->properties()
(lldb) p $7.list
(property_list_t *const) $8 = 0x0000000100002230
(lldb) p *$8
(property_list_t) $9 = {
entsize_list_tt<property_t, property_list_t, 0> = {
entsizeAndFlags = 16
count = 1
first = (name = "lg_name", attributes = "T@\"NSString\",C,N,V_lg_name")
}
}
總結(jié):類的實例方法和類的屬性都存在bits中勒极,我們發(fā)現(xiàn)類的類方法和類的成員變量卻沒有打印是掰,我們可以思考一下,它們存在哪里呢辱匿?類的類方法會不會存在元類里面呢键痛?下一節(jié)我們接著探索一下這個內(nèi)容炫彩。