說到對象,什么是對象?
由于文章的連貫性、強烈建議先看看之前的文章:Objective-C 中類的數據結構 與 Objective-C 中實例所占內存的大小。
在面向對象編程中,有兩個重要的概念:類與對象。在實際的內存中 類 又是以什么樣的形式存在的呢?
萬物皆屌絲,不對、是萬物皆對象。
一、對象
在 OC 中什么是對象?有很長一段時間堅信認為 +alloc 出來的才是對象。如果說平時這么說還行,一旦放到面試的時候這樣來回答、那恐怕就不行,因為這個答案是不全的。主要分為以下三種:
- 1、instance 對象,也稱實例對象。
- 2、class 對象,也稱類對象。
- 3、meta-class 對象,也稱元類對象。
沒錯,就是這三個對象。突然感覺哪里不對勁,不是說好的 block 也是一種特殊的對象么?對,也沒有錯,但是今天暫時不討論這個 block 對象。
接下來、將會討論這些對象中都包含了什么樣的信息,這些信息都是如何關聯起來的,成員變量是存在哪里的,類方法與實例方法是存在哪里的,如何找到 superclass 的。。。。。關于這些問題,將會一一的梳理一遍。
1.1 instance 對象
1.1.1 什么是 instance 對象
總而言之就是通過 +alloc 之后的的都是 instance 對象。但是這里還需要強調一點的是: instance 對象并不僅僅是 NSObject 的對象,還有一個代理類 NSProxy,也是能創建 instance 對象。
關于 NSProxy,大家應該高度重視,當面試官問你在 OC 中有什么代理類的時候,別說不知道什么代理類,就知道代理協議(delegate)。如果你說不知道,那有一點可以很肯定,YYKit 那么優秀的框架都不去學習一下,對于大廠來說,恐怕會遭到鄙視的。具體的可以參考 YYKit 中的 YYWeakProxy。看完 YYWeakProxy 之后,你還會學到另一個技能:如何處理 OC 中定時器的循環引用。這兩個問題在面試中,含金量都不低。在接下來的介紹中,不再提及 NSProxy 類。
代碼中是如何獲取一個對象的:
// 創建一個對象
NSObject* obj = [NSObject alloc];
1.1.2 instance 對象中的信息
instance 對象中都包含什么呢?通過前面的兩篇文章 Objective-C 中類的數據結構 與 Objective-C 中實例所占內存的大小得知,僅有成員變量,沒有其它的,其中他們的成員變量是有繼承關系的。比如 Person 類繼承于 NSObject,那么 Person 的 instance 對象中就會繼承 NSObject 中的所有成員變量。
1.2 class 對象
1.2.1 獲取 class 對象
class 對象是怎么被創建的?畢竟在開發過程中也沒有見過通過 +alloc 的方式創建之。但是知道怎么去獲取一個 class 對象,見下面的代碼:
{
// 創建對象
ClsObject* cObj = [[ClsObject alloc] init];
// 獲取 class 的 所有方法
[self fetchClassWiothCObj:cObj];
}
// 獲取 class 的 所有方法
- (void)fetchClassWiothCObj:(ClsObject*)cObj {
Class obcCls1 = [cObj class];
Class obcCls2 = [ClsObject class];
Class obcCls3 = object_getClass(cObj);
NSLog(@"%@, %@, %@", NSStringFromClass(obcCls1), NSStringFromClass(obcCls2), NSStringFromClass(obcCls3));
// 打印結果: ClsObject, ClsObject, ClsObject
NSLog(@"%p, %p, %p", obcCls1, obcCls2, obcCls3);
// 打印結果: 0x10f52dd30, 0x10f52dd30, 0x10f52dd30
}
以上代碼中的 ClsObject 是一個直接繼承于 NSObject 的 Class。
沒錯,不管是通過什么方法獲取的 class 對象都是一樣的,包括地址。說明在一個項目中一個 Class 僅有一個對象。但是上面的三種獲取 class 對象的方式有什么不一樣呢?
第一種與第二種是通過方法獲取的,直接獲取的是當前 instance 的 Class,但是第三種方式不一樣,這種方式是獲取當前 instance 的 isa 的值。
可以這樣做一個實驗,給上面的 cObj 做一個 KVO 監聽,我們再看一下打印結果,會發現打印的結果變成了這樣的:
// 打印結果:ClsObject, ClsObject, NSKVONotifying_ClsObject
// 打印結果:0x102e5ee10, 0x102e5ee10, 0x60000011a820
是的,第三個值變了,變成了 NSKVONotifying_ClsObject。同時還發現,所有同一個 Class 的 instance 注冊的 KVO 的 NSKVONotifying_ 的 class 對象的值也是一樣的。
1.2.2 class 對象中的信息
- 1、isa
- 2、superclass
- 3、屬性 property
- 4、instance 方法
- 5、協議 protocal
- 6、成員變量,這里的成員變量信息并不是一個 instance 中成員變量的值,而是指在這個 Class 中有哪些成員變量,是 NSSting 的,還是 int 類型的。
-
7、其它
。。。。。。。
1.3 meta-class 對象
1.3.1 獲取 meta-class 對象
同理在開發中是不會手動去 +alloc 一個元類對象,可以通過 object_getClass 函數獲取 class 對象的 isa 類獲取之。代碼如下:
// 獲取元類對象
- (void)metaClass:(ClsObject*)cObj {
// 獲取一個對象的 isa
Class obcISA = object_getClass(cObj);
// 獲取元類對象
Class metaClass = object_getClass(obcISA);
NSLog(@"%p, %@", metaClass, NSStringFromClass(metaClass));
}
會發現,元類還是當前的 Class,但是是另一個對象地址。
其次,不管是 class 對象還是元類對象,其類型都是 Class,說明在內存結構上是一致的。但是其包含的信息含義是不一樣,其用途也不一樣。
1.3.2 meta-class 對象中的信息
- 1、isa
- 2、superclass
- 3、類方法信息
- 4、其它
1.4 對象總結
- 1、總共有三種對象:instance 對象、class 對象與 meta-class 對象。
- 2、成員變量的值都存于 instance 對象中。
- 3、屬性、instance (實例)方法、協議 protocol、成員變量都存于 class 對象中。
- 4、類方法都存于 meta-class 對象中。
二、關于 isa
以上的三種對象是如何關聯起來的呢?是通過 isa 關聯的:
instance 對象的 isa 的值是 class 對象,class 對象的 isa 的值是 meta-class對象。
盡然實例方法是存在 class 對象中,那么當給一個 instance 對象發送消息的時候,是如何找到具體的方法實現的呢?
當調用實例方法的時候, 通過 instance 對象中的 isa 找到 class,找到對應的實例方法的實現。
同理,類方法的調用也是一樣:
當調用類方法的時候,通過 class 對象的 isa 指針找到 meta-class,并找到對應的方法實現。
不管是調用 Class 方法還是對象方法都是消息發送,這里有一個面試題是這樣問的:OC 中的消息發送的本質是什么?在之前我是這樣的回答的:通過 SEL 去找對應的 IMP 實現,首先是從當前 Class(meta-class) 尋找,如果一旦找不到就會到父類尋找,當所有的都沒有找到那么會啟動消息轉發機制,一旦找到了、那么會將當前的 SEL 與 IMP 緩存起來方便下一次查詢。之前一直以為這樣的回答夠完美的了,但是現在看來需要再加一點專業術語會更加的完善。消息轉發的本質是通過 isa 查找對應的 IMP 實現。然后加上之前的回答即可。
為什么要強調這一點呢?難道在 OC 中還有不需要 isa 直接發送消息的??是的、有一個方法被調用就沒有通過 isa 的查詢,那就是 +load 方法。在很久之前也一直有一個疑問:為什么在分類中重寫了 +load 方法之后,原生 Class 的+load 方法還能被調用。原來是因為 +load 方法的調用邏輯是在 dyld 加載階段,一旦檢測到當前的 Class 或者其分類重寫了 +load 直接通過 IMP 地址進行調用。所以這種情況就不會出現原生 Class 的 IMP 后移從而導致沒有機會被調用的情況。
三、關于 superclass
superclass 指針 是相對于 class 對象 與 meta-class 對象 來說的。這個指針有什么作用呢?
定義兩個 Class:Person 繼承于 NSObject,Student 繼承于 Person。現在有一個場景,通過 Student 的 instance 對象調用 Person 中實現的實例方法,具體的調用過程如下:
通過 Student 類的 instance 對象 的 isa 找到對應 Student 類的 class 對象,但是沒有找到相關的實現,系統會繼續到 superclass 中找,于是會到 Person 類的 class 對象 中找到具體的實現,并調用。
類方法的調用,也是一樣。
四、 isa 與 superclass
美圖欣賞,以上所說的都是為了能看懂這張圖片:
由圖可知:
1、isa
- 1、instance 的 isa 指向 class
- 2、class 的 isa 指向 meta-class
- 3、meta-class 的 isa 指向基類的 meta-class
2、superclass
- 1、class 的 superclass 指向父類的 class,如果沒有父類,superclass 為 nil
- 2、neta-class 的 superclass 指向父類的 meta-class,基類的 meta-class 的 super 指向基類的 class
3、 方法調用軌跡
instance 對象: isa 找到class,方法如果不存在,就通過 superclass找父類。
class 對象: isa 找到meta-class,方法如果不存在,就通過 superclass 找父類。
五、isa、class 與元類(metaClass)的關系求證
上面說到這樣的一句:
instance 對象的 isa 的值是 class 對象,class 對象的 isa 的值是 meta-class對象。
通過上圖也已經有所提現了,再把上圖做一個標識,如下:
接下來就是證明一下這 5 條線的正確性。具體代碼如下:
// instance
HGObject* obj = [[HGObject alloc] init];
// class 第一根線
Class objCls = object_getClass(obj);
// metaClass 第二根線
Class objMetaCls = object_getClass(objCls);
// rootMetaCls (元類的父元類) 第三根線
Class rootMetaCls0 = class_getSuperclass(objMetaCls);
// 與元類的 Class 第四根線
Class rootMetaCls1 = object_getClass(objMetaCls);
// 根元類的 Class
Class rootMetaCls = object_getClass(rootMetaCls0);
NSLog(@"\ninstance = %p\nobjCls = %p \nobjMetaCls = %p\nrootMetaCls0 = %p\nrootMetaCls1 %p\nrootMetaCls = %p", obj, objCls, objMetaCls, rootMetaCls0, rootMetaCls1, rootMetaCls);
其中 HGObject 是直接繼承 NSObject 的類。打印結果:
instance = 0x604000012430
objCls = 0x10619eea8
objMetaCls = 0x10619ee80
rootMetaCls0 = 0x107147e58
rootMetaCls1 0x107147e58
rootMetaCls = 0x107147e58
注意一下后面的三個值, 都是一樣的。到現在應該已經理清了。但是在上面的代碼中,沒有看到 isa 相關的,我們僅僅是獲取了對應對象的 類型(Class)而已。現在想要看看具體的 isa 的值打的是多少,在代碼中是很難看到的,如果一定要在代碼中查看,那也是可以的。接下來使用 LLDB 來查看看。比如想要查看 instance 的 isa 值,可以這么操作:
可以看出 obj 的 isa 就是其對應的 objCls 的值。同理可以查看一下 objCls 的 isa 的值是不是 objMetaCls。在操作的過程中會發現這樣的提示:
我有一個解決方案,將 objCls 轉成一個 NSObject 即可,如下:
// 將 Class 轉成 NSObject
NSObject* clsObj = (NSObject*)objCls;
NSLog(@"%p", clsObj);
然后通過查詢 clsObj 的 isa 的值,就是 objCls 的 isa 的值。
判斷是否為元類的方法:
if (class_isMetaClass(objMetaCls)) {
NSLog(@"是元類");
} else {
NSLog(@"不是");
}
重點的問題來了!!!!!!!
以上的操作, 都是在 iOS 項目中的模擬器的結果,但是如果換成 Mac 項目,獲取換成真機,結果就不一樣了。比如,我將上面的代碼放到 Mac 中(僅僅是部分代碼), 如下:
// insert code here...
HGObject* obj = [[HGObject alloc] init];
// class 第一根線
Class objCls = object_getClass(obj);
// 打印
NSLog(@"\nobj = %p\nobjCls = %p", obj, objCls);
打印結果是這樣的:
obj = 0x10050d8d0
objCls = 0x100001140
但是查看 isa 發現這樣的結果:
結果不一樣了!!!!
是的,是這樣的。在一些系統下,做了一個轉換,什么樣的轉換呢?先看一下結果:
厲害了,這是一個巧合吧,這個巧合不太巧合,是一個規律。在一些系統下的 isa 要做一個與運算才能得到真實類型的具體值。面使用的值是 0x00007ffffffffff8,這是在 MAC 上的,但是在真機上是不一樣的。具體的定義,可以在源碼中找到(已經刪除其它定義):
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# else
# error unknown architecture for packed isa
# endif
// SUPPORT_PACKED_ISA
#endif
在上面使用的就是 ISA_MASK 的值,在這路也可以看出。在其它的地方也會用到同樣的轉換:ISA_MAGIC_MASK 與 ISA_MAGIC_VALUE。
關于 LLDB 的更多使用,可以參考:Xcode 常用 LLDB 指令