類的分析
- 準備工作,我們先創建兩個類繼承
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
文件中如下設置
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;
}
-
類結構的分析
在mian.m
文件LGStudent *student = [LGStudent alloc];
處打上斷點運行一下項目工程,使用lldb
進行調試
lldb調試過程及輸出結果- 首先我們可以根據lldb命令得到
person的內存分布
,我們知道0x001d8001000022dd
是person
的內存指針,接著使用這個指針&0x00007ffffffffff8ULL
,可以獲取類的相關信息$3 = 0x00000001000022d8
。 - 接著
po 0x00000001000022d8
來打印類的信息,我們發現結果為LGPerson
; -
x/4gx 0x00000001000022d8
,來打印LGPerson
的內存情況,我們同樣的可以拿到類的isa指針地址0x00000001000022b0
; - 下一步,
p/x 0x00000001000022b0 & 0x00007ffffffffff8ULL
,我們來獲取類的信息,我們得到$5 = 0x00000001000022b0
類的指針地址; - 下一步:通過
po 0x00000001000022b0
,發現結果還是LGPerson
,這是為什么呢?這里先簡單說一下這個LGPerson
是元類;下面小節在詳細說明元類的問題。
- 首先我們可以根據lldb命令得到
二、元類,主要有以下幾點說明:
我們都知道
對象的isa
是指向類
,類
其實也是一個對象
,可以稱為類對象
,其isa的位域
指向蘋果定義的元類
。元類
是系統
設置的,其定義和創建都是由編譯器
完成,在這個過程中,類的歸屬
來自于`元類。元類
是類對象
的類,每個類都有一個獨一無二的元類用來存儲 類方法的相關信息。元類
本身是沒有名稱
的,由于與類相關聯
,所以使用了同類名一樣
的名稱。
總結:由上圖的打印結果我們可以得出如下結論:對象 --> 類 --> 元類 --> NSobject, NSObject 指向自身
。
三、isa走位 & 繼承關系
根據上面的探索以及各種驗證,對象、類、元類、根元類
的關系如下圖所示
isa的走向有以下幾點說明:
-
實例對象(Instance of Subclass)
的isa
指向類(class)
-
類對象(class) isa
指向元類(Meta class)
-
元類(Meta class)的isa
指向根元類(Root metal class)
-
根元類(Root metal class) 的isa
指向它自己本身
,形成閉環,這里的根元類就是NSObject。
superclass(即繼承關系)的走向也有以下幾點說明:
-
類(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走位鏈
- 類的繼承關系鏈:
LGStudent(子類) --> CJLPerson(父類) --> NSObject(根類)--> nil
- 元類的繼承關系鏈:
LGStudent(子元類) --> CJLPerson(父元類) --> NSObject(根元類)--> NSObject(根類)--> nil
四、objc_class & objc_object
在分析objc_class & objc_object
我們先引入一個問題,為什么類和對象都有isa屬性
呢?
我們先將main.m文件
編譯為main.cpp
來分析一下這個問題。我們根據clang編譯的c++源碼
可以看出NSObject的底層編譯
是NSObject_IMPL結構體
,并且對象含有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
在源碼中搜索到兩種相關源碼 - 第一種已經不再使用了,并且和我們使用
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();
}
};
我們發現objc_class
繼承自objc_object
,下面我們再看看objc_object源碼
,
-
objc_object
源碼
struct objc_object {
private:
isa_t isa;
};
總結:
- 我們根據源碼發現
objc_object結構體
定義了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
表示指向,來自于當前的objc_object
。 - 所以所有的對象都是以
objc_object為模板繼承過來
的。 - 因為
對象
是 來自NSObject(OC)
,但是真正到底層的 是一個objc_object(C/C++)的結構體類型
,所以objc_object
與對象的關系
是繼承關系
。
objc_class
、objc_object
、isa
、object
、NSObject
等的整體的關系,如下圖所示
類的方法
我們根據下面類的底層實現源碼來探索一下,類的實例方法存儲在哪里,來具體探索一下類的結構是怎樣的?
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits;
// ......部分代碼不在展示
}
- 在探索之前我們需要補充一下知識,關于
內存偏移
,我們先使用實例說明一下這個內存偏移
;
【普通指針】
//普通指針
int a = 10; //變量
int b = 10;
NSLog(@"%d -- %p", a, &a);
NSLog(@"%d -- %p", b, &b);
輸出的結果我們可以從控制臺看出,a和b兩個指針指向了同一片的存儲著10的空間。
a、b
都指向10
,但是a、b的地址``不一樣,這是一種拷貝
,屬于值拷貝,也稱為淺拷貝
a,b的地址
之間相差4
個字節,這取決于a、b的類型
【對象指針】
LGPerson *p1 = [LGPerson alloc]; // p1 是指針
LGPerson *p2 = [LGPerson alloc];
NSLog(@"%d -- %p", p1, &p1);
NSLog(@"%d -- %p", p2, &p2);
輸出結果
p1、p2
是指針,p1
是 指向[LGPerson alloc]創建的空間地址
,即內存地址,p2
同理&p1、&p2
是 指向p1、p2對象指針的地址
,這個指針 就是 二級指針
【數組指針】
//數組指針
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);
輸出結果-
&c
和&c[0]
都是取這個數組的首地址
,所以``數組名等同于首地址
; -
&c
與&c[1]
相差4個字節
,地址之間相差的字節數
,主要取決于存儲的數據類型可以通過
首地址+偏移量取出數組中的其他元素,其中
偏移量是
數組的下標,
內存中首地址實際移動的字節數等于
偏移量 * 數據類型字節數`;
計算類結構的內存大小
通過上面類結構的源碼我們來計算一下類的大小
-isa屬性
:繼承自objc_object
的isa
,占 8
字節;
-
superclass
屬性:Class
類型,Class
是由objc_object
定義的,是一個指針
,占8
字節 -
cache
屬性:是cache_t
結構體類型,我們應該按照計算結構體內存大小的規則來計算,而結構體指針
才是8字節
;下面代碼可以計算出cache占16字節
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
explicit_atomic<struct bucket_t *> _buckets;// 是一個結構體指針類型,占8字節
explicit_atomic<mask_t> _mask;//是mask_t 類型,而 mask_t 是 unsigned int 的別名,占4字節
#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個字節
#endif
uint16_t _occupied; //是uint16_t類型,uint16_t是 unsigned short 的別名,占 2個字節
//省略部分代碼
}
-
bits屬性:只有首地址經過上面
3個屬性的內存大小總和的平移,才能獲取到
bits`;
類結構中方法分析流程
(lldb) p/x LGPerson.class
(Class) $0 = 0x00000001000022f0 LGPerson
(lldb) //平移32個字節
error: '//平移32個字節' 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) 通過結構體的data()來獲取bits
error: '通過結構體的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內容
error: '打印list內容' 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)
- 關于類的屬性
(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")
}
}
總結:類的實例方法和類的屬性都存在bits中,我們發現類的類方法和類的成員變量卻沒有打印,我們可以思考一下,它們存在哪里呢?類的類方法會不會存在元類里面呢?下一節我們接著探索一下這個內容。