本文的主要目的是分析 類 & 類的結構,整篇都是圍繞一個類
展開的一些探索
類 的分析
類的分析 主要是分析 isa
的走向 以及 繼承
關系
準備工作
定義兩個類
- 繼承自
NSObject
的類CJLPerson
,
@interface CJLPerson : NSObject
{
NSString *hobby;
}
@property (nonatomic, copy) NSString *cjl_name;
- (void)sayHello;
+ (void)sayBye;
@end
@implementation CJLPerson
- (void)sayHello
{}
+ (void)sayBye
{}
@end
- 繼承自
CJLPerson
的類CJLTeacher
@interface CJLTeacher : CJLPerson
@end
@implementation CJLTeacher
@end
- 在main中分別用兩個定義兩個對象:
person & teacher
int main(int argc, const char * argv[]) {
@autoreleasepool {
//ISA_MASK 0x00007ffffffffff8ULL
CJLPerson *person = [CJLPerson alloc];
CJLTeacher *teacher = [CJLTeacher alloc];
NSLog(@"Hello, World! %@ - %@",person,teacher);
}
return 0;
}
元類
首先,我們先通過一個案例的lldb調試先引入元類
- 在main中CJLTeacher部分加一個斷點,運行程序
-
開啟lldb調試,調試的過程如下圖所示
lldb調試過程
根據調試過程,我們產生了一個疑問
:為什么圖中的p/x 0x001d8001000022dd & 0x00007ffffffffff8ULL
與 p/x 0x00000001000022b0 & 0x00007ffffffffff8ULL
中的類信息打印出來都是CJLPerson
?
-
0x001d8001000022dd
是person
對象的isa指針地址
,其&
后得到的結果
是創建person
的類CJLPerson
-
0x00000001000022b0
是isa中獲取的類信息所指的類的isa
的指針地址,即CJLPerson類的類
的isa
指針地址,在Apple中,我們簡稱CJLPerson類的類
為元類
- 所以,兩個打印都是
CJLPerson
的根本原因就是因為元類
導致的
元類的說明
下面來解釋什么是元類
,主要有以下幾點說明:
我們都知道
對象
的isa
是指向類
,類
的其實也是一個對象
,可以稱為類對象
,其isa
的位域指向蘋果定義的元類
元類
是系統
給的,其定義
和創建
都是由編譯器
完成,在這個過程中,類
的歸屬
來自于元類
元類
是類對象
的類
,每個類
都有一個獨一無二的元類
用來存儲類方法的相關信息
。元類
本身是沒有名稱的
,由于與類
相關聯
,所以使用了同類名一樣的名稱
下面通過lldb
命令來探索元類的走向
,也就是isa
的走位
,如下圖所示,可以得出一個關系鏈:對象 --> 類 --> 元類 --> NSobject, NSObject 指向自身
總結
從圖中可以看出
-
對象
的isa
指向類
(也可稱為類對象
) -
類
的isa
指向元類
-
元類
的isa
指向根元類
,即NSObject
-
根元類
的isa
指向 它自己
NSObject到底有幾個?
從圖中可以看出,最后的根元類
是NSObject
,這個NSObject
與我們日開開發中所知道的NSObject
是同一個嗎?
有以下兩種驗證方式
- 【方式一】
lldb
命令驗證 - 【方式二】
代碼
驗證
【方式一】lldb命令驗證
我們也通過lldb調試,來驗證這兩個NSObject是否是同一個,如下圖所示
從圖中可以看出,最后NSObject
類的元類
也是NSObject
,與上面的CJLPerson
中的根元類
(NSObject)的元類
,是同一個,所以可以得出一個結論:內存中只存在存在一份根元類NSObject,根元類的元類是指向它自己
【方式二】代碼驗證
通過三種不同的方式獲取類,看他們打印的地址是否相同
//MARK:--- 分析類對象內存 存在個數
void testClassNum(){
Class class1 = [CJLPerson class];
Class class2 = [CJLPerson alloc].class;
Class class3 = object_getClass([CJLPerson alloc]);
NSLog(@"\n%p-\n%p-\n%p-\n%p", class1, class2, class3);
}
以下是代碼運行的結果
從結果中可以看出,打印的地址都是同一個
,所以NSObject只有一份
,即NSObject(根元類)
在內存中永遠只存在一份
[面試題]:類存在幾份?
由于類的信息在內存中永遠只存在一份,所以 類對象只有一份
著名的 isa走位 & 繼承關系 圖
根據上面的探索以及各種驗證,對象、類、元類、根元類
的關系如下圖所示
isa走位
isa的走向有以下幾點說明:
實例對象(Instance of Subclass)
的isa
指向類(class)
類對象(class)
isa
指向元類(Meta class)
元類(Meta class)
的isa
指向根元類(Root metal class)
根元類(Root metal class)
的isa
指向它自己
本身,形成閉環
,這里的根元類
就是NSObject
superclass走位
superclass(即繼承關系)的走向也有以下幾點說明:
-
類
之間 的繼承
關系:類(subClass)
繼承自父類(superClass)
父類(superClass)
繼承自根類(RootClass)
,此時的根類是指NSObject
根類
繼承自nil
,所以根類
即NSObject
可以理解為萬物起源
,即無中生有
-
元類
也存在繼承
,元類之間的繼承關系如下:子類的元類(metal SubClass)
繼承自父類的元類(metal SuperClass)
父類的元類(metal SuperClass)
繼承自根元類(Root metal Class
根元類(Root metal Class)
繼承于根類(Root class)
,此時的根類是指NSObject
- 【注意】
實例對象
之間沒有繼承關系
,類
之間有繼承關系
舉例說明
以前文提及的的CJLTeacher及對象teacher
、CJLPerson及對象person
舉例說明,如下圖所示
-
isa 走位鏈(兩條)
teacher的isa走位鏈:
teacher(子類對象) --> CJLTeacher (子類)--> CJLTeacher(子元類) --> NSObject(根元類) --> NSObject(跟根元類,即自己)
person的isa走位圖:
person(父類對象) --> CJLPerson (父類)--> CJLPerson(父元類) --> NSObject(根元類) --> NSObject(跟根元類,即自己)
-
superclass走位鏈(兩條)
類的繼承關系鏈:
CJLTeacher(子類) --> CJLPerson(父類) --> NSObject(根類)--> nil
元類的繼承關系鏈:
CJLTeacher(子元類) --> CJLPerson(父元類) --> NSObject(根元類)--> NSObject(根類)--> nil
objc_class & objc_object
isa走位我們理清楚了,又來了一個新的問題:為什么 對象
和 類
都有isa屬性
呢?這里就不得不提到兩個結構體類型:objc_class
& objc_object
下面在這兩個結構體的基礎上,對上述問題進行探索。
在上一篇文章iOS-底層原理 07:isa與類關聯的原理中,使用clang
編譯過main.m文件,從編譯后的c++文件中可以看到如下c++源碼
-
NSObject
的底層編譯是NSObject_IMPL
結構體,- 其中
Class
是isa
指針的類型,是由objc_class
定義的類型, - 而
objc_class
是一個結構體。在iOS中,所有的Class
都是以objc_class
為模板創建的`
- 其中
struct NSObject_IMPL {
Class isa;
};
typedef struct objc_class *Class;
-
在objc4源碼中搜索
objc_class
的定義,源碼中對其的定義有兩個版本-
舊版
位于runtime.h
中,已經被廢除
舊版objc_class定義 -
新版 位于
objc-runtime-new.h
,這個是objc4-781
最新優化的,我們后面的類的結構分析也是基于新版來分析的。
新版objc_class定義
從新版的定義中,可以看到objc_class
結構體類型是繼承自objc_object
的,
-
-
在objc4源碼中搜索
objc_object (或者 objc_object {
,這個類型也有兩個版本- 一個位于
objc.h
,沒有被廢除,從編譯的main.cpp
中可以看到,使用的這個版本的objc_object
objc.h中的objc_object定義 - 位于
objc-privat.h
objc-privat.h中的objc_object定義
- 一個位于
以下是編譯后的main.cpp
中的objc_object
的定義
struct objc_object {
Class _Nonnull isa __attribute__((deprecated));
};
【問題】objc_class 與 objc_object 有什么關系?
通過上述的源碼查找以及main.cpp中底層編譯源碼,有以下幾點說明:
結構體類型
objc_class
繼承自objc_object
類型,其中objc_object
也是一個結構體,且有一個isa
屬性,所以objc_class
也擁有了isa
屬性mian.cpp底層編譯文件中,
NSObject
中的isa
在底層是由Class
定義的,其中class
的底層編碼來自objc_class
類型,所以NSObject
也擁有了isa
屬性NSObject
是一個類,用它初始化一個實例對象objc
,objc 滿足objc_object
的特性(即有isa屬性),主要是因為isa
是由NSObject
從objc_class
繼承過來的,而objc_class
繼承自objc_object
,objc_object
有isa
屬性。所以對象
都有一個isa
,isa表示指向
,來自于當前的objc_object
objc_object(結構體)
是 當前的根對象
,所有的對象
都有這樣一個特性objc_object
,即擁有isa屬性
【百度面試題】objc_object 與 對象的關系
所有的
對象
都是以objc_object
為模板繼承
過來的所有的對象 是 來自
NSObject
(OC) ,但是真正到底層的 是一個objc_object(C/C++)
的結構體類型
【總結】 objc_object
與 對象
的關系
是 繼承
關系
總結
所有的
對象
+類
+元類
都有isa
屬性所有的
對象
都是由objc_object
繼承來的-
簡單概括就是
萬物皆對象
,萬物皆來源于objc_object
,有以下兩點結論:所有以
objc_object
為模板 創建的對象
,都有isa
屬性所有以
objc_class
為模板,創建的類
,都有isa
屬性
-
在結構層面可以通俗的理解為
上層OC
與底層
的對接
:-
下層
是通過結構體
定義的模板
,例如objc_class、objc_object
-
上層
是通過底層的模板創建
的 一些類型,例如CJLPerson
-
objc_class、objc_object、isa、object、NSObject
等的整體的關系,如下圖所示
類結構分析
主要是分析類信息
中存儲了哪些內容
補充知識-內存偏移
在分析類結構之前,需要先了解內存偏移,因為類信息中訪問時,需要使用內存偏移
【普通指針】
//普通指針
int a = 10; //變量
int b = 10;
NSLog(@"%d -- %p", a, &a);
NSLog(@"%d -- %p", b, &b);
打印結果如下圖所示
a、b都指向10,但是a、b的
地址不一樣
,這是一種拷貝,屬于值拷貝
,也稱為深拷貝
a,b的地址之間相差 4 個字節,這取決于a、b的類型
其地址指向如圖所示
【對象指針】
//對象
CJLPerson *p1 = [CJLPerson alloc]; // p1 是指針
CJLPerson *p2 = [CJLPerson alloc];
NSLog(@"%d -- %p", p1, &p1);
NSLog(@"%d -- %p", p2, &p2);
打印結果如圖所示
p1、p2 是指針,
p1
是 指向[CJLPerson 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
個字節,地址之間相差的字節數,主要取決于存儲的數據類型
- 可以通過
首地址+偏移量
取出數組中的其他元素,其中偏移量是數組的下標,內存中首地址實際移動的字節數 等于 偏移量 * 數據類型字節數
其指針指向如下所示
探索類信息中都有哪些內容
探索類信息
中有什么時,事先我們并不清楚類
的結構
是什么樣的,但是我們可以通過類
得到一個首地址
,然后通過地址平移
去獲取里面所有的值
根據前文提及的objc_class
的新版定義(objc4-781
版本)如下,有以下幾個屬性
struct objc_class : objc_object {
// Class ISA; //8字節
Class superclass; //Class 類型 8字節
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
//....方法部分省略,未貼出
}
isa
屬性:繼承自objc_object
的isa
,占8
字節superclass
屬性:Class
類型,Class
是由objc_object
定義的,是一個指針
,占8
字節cache
屬性:簡單從類型class_data_bits_t
目前無法得知,而class_data_bits_t
是一個結構體
類型,結構體
的內存大小
需要根據內部的屬性
來確定,而結構體指針才是8字節
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; // 是一個結構體指針類型,占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; //是指針,占8字節
mask_t _mask_unused; //是mask_t 類型,而 mask_t 是 uint32_t 類型定義的別名,占4字節
#if __LP64__
uint16_t _flags; //是uint16_t類型,uint16_t是 unsigned short 的別名,占 2個字節
#endif
uint16_t _occupied; //是uint16_t類型,uint16_t是 unsigned short 的別名,占 2個字節
-
計算
前兩個屬性
的內存大小
,有以下兩種情況,最后的內存大小總和都是12
字節-
【情況一】
if
流程buckets
類型是struct bucket_t *
,是結構體指針
類型,占8
字節mask
是mask_t
類型,而mask_t
是unsigned int
的別名,占4
字節
-
【情況二】
elseif
流程_maskAndBuckets
是uintptr_t
類型,它是一個指針
,占8
字節_mask_unused
是mask_t
類型,而mask_t
是uint32_t
類型定義的別名,占4
字節
-
_flags
是uint16_t
類型,uint16_t是unsigned short
的別名,占2
個字節_occupied
是uint16_t
類型,uint16_t是unsigned short
的別名,占2
個字節
總結:所以最后計算出cache類的內存大小 = 12 + 2 + 2 = 16
字節
獲取bits
所以有上述計算可知,想要獲取bits
的中的內容,只需通過類
的首地址平移32
字節即可
以下是通過lldb
命令調試的過程
- 獲取類的首地址有兩種方式
通過
p/x CJLPerson.class
直接獲取首地址通過
x/4gx CJLPerson.class
,打印內存信息獲取
-
其中的
data()
獲取數據,是由objc_class
提供的方法
data方法 從
$2
指針的打印結果中可以看出bits
中存儲的信息,其類型是class_rw_t
,也是一個結構體類型。但我們還是沒有看到屬性列表、方法列表
等,需要繼續往下探索
探索 屬性列表,即 property_list
通過查看class_rw_t
定義的源碼發現,結構體
中有提供
相應的方法
去獲取 屬性列表、方法列表
等,如下所示
在獲取bits
并打印bits
信息的基礎上,通過class_rw_t
提供的方法,繼續探索 bits
中的屬性列表
,以下是lldb 探索的過程圖示
p $8.properties()
命令中的propertoes
方法是由class_rw_t
提供的,方法中返回
的實際類型
為property_array_t
由于list的類型是
property_list_t
,是一個指針,所以通過p *$10
獲取內存中的信息
,同時也證明bits
中存儲了property_list
,即屬性列表p $11.get(1)
,想要獲取CJLPerson
中的成員變量``bobby
, 發現會報錯,提示數組越界
了,說明property_list
中只有 一個屬性cjl_name
【問題】探索成員變量的存儲
由此可得出property_list 中只有屬性,沒有成員變量
,屬性與成員變量的區別就是有沒有set、get方法,如果有,則是屬性,如果沒有,則是成員變量。
那么問題來了,成員變量存儲在哪里?為什么會有這種情況?請移至文末的分析與探索
探索 方法列表,即methods_list
準備工作:在前文提及的CJLPerson中增加兩個方法(實例方法 & 類方法)
//CJLPerson.h
@property (nonatomic, copy) NSString *cjl_name;
- (void)sayHello;
+ (void)sayBye;
@end
//CJLPerson.m
@implementation CJLPerson
- (void)sayHello
{}
+ (void)sayBye
{}
@end
也是通過lldb調試來獲取方法列表,步驟如圖所示
通過
p $4.methods()
獲得具體的方法列表
的list
結構,其中methods
也是class_rw_t
提供的方法通過打印的
count = 4
可知,存儲了4
個方法,可以通過p $7.get(i)
內存偏移
的方式獲取單個方法,i 的范圍是0-3
如果在打印
p $7.get(4)
,獲取第五個方法,也會報錯,提示數組越界
新問題的探索
【問題】探索成員變量的存儲
由上面的屬性列表分析可得出property_list 中只有屬性,沒有成員變量
,那么問題來了,成員變量存儲在哪里?為什么會有這種情況?
通過查看objc_class
中bits
屬性中存儲數據的類class_rw_t
的定義發現,除了methods、properties、protocols
方法,還有一個ro
方法,其返回類型是class_ro_t
,通過查看其定義,發現其中有一個ivars
屬性,我們可以做如下猜測:是否成員變量就存儲在這個ivar_list_t
類型的ivars
屬性中呢?
下面是lldb的調試過程
-
class_ro_t
結構體中的屬性如下所示,想要獲取ivars
,需要ro的首地址平移48
字節
struct class_ro_t {
uint32_t flags; //4
uint32_t instanceStart;//4
uint32_t instanceSize;//4
#ifdef __LP64__
uint32_t reserved; //4
#endif
const uint8_t * ivarLayout; //8
const char * name; //1 ? 8
method_list_t * baseMethodList; // 8
protocol_list_t * baseProtocols; // 8
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
//方法省略
}
通過圖中可以看出,獲取的ivars
屬性,其中的count
為2
,通過打印發現 成員列表中除了有hobby,還有name,所以可以得出以下一些結論:
通過
{}
定義的成員變量
,會存儲在類的bits
屬性中,通過bits --> data() -->ro() --> ivars
獲取成員變量列表
,除了包括成員變量,還包括屬性定義的成員變量通過
@property
定義的屬性,也會存儲在bits屬性中,通過bits --> data() --> properties() --> list
獲取屬性列表
,其中只包含屬性
【問題】探索類方法的存儲
由此可得出methods list 中只有 實例方法,沒有類方法
,那么問題來了,類方法存儲在哪里?為什么會有這種情況?下面我們來仔細分析下
在文章前半部分,我們曾提及了元類
,類對象
的isa
指向就是元類
,元類
是用來存儲類
的相關信息的,所以我們猜測:是否類方法存儲在元類的bits中呢
?可以通過lldb
命令來驗證我們的猜測。下圖是lldb命令的調試流程
通過圖中元類方法列表
的打印結果,我們可以知道,我們的猜測是正確的,所以可以得出以下結論:
類
的實例方法
存儲在類的bits屬性
中,通過bits --> methods() --> list
獲取實例方法列表
,例如CJLPersong
類的實例方法sayHello
就存儲在CJLPerson類的bits
屬性中,類中的方法列表
除了包括實例方法
,還包括屬性的set方法
和get方法
類
的類方法
存儲在元類的bits屬性
中,通過元類bits --> methods() --> list
獲取類方法列表
,例如CJLPerson
中的類方法sayBye
就存儲在CJLPerson
類的元類
(名稱也是CJLPerson)的bits
屬性中