Objective-C 中的對象 & isa & superclass & 元類(metaClass)

說到對象,什么是對象?

由于文章的連貫性、強烈建議先看看之前的文章: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

美圖欣賞,以上所說的都是為了能看懂這張圖片:


class.png

由圖可知:

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對象。

通過上圖也已經有所提現了,再把上圖做一個標識,如下:

image.png

接下來就是證明一下這 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 來查看看。比如想要查看 instanceisa 值,可以這么操作:

image.png

可以看出 objisa 就是其對應的 objCls 的值。同理可以查看一下 objClsisa 的值是不是 objMetaCls。在操作的過程中會發現這樣的提示:

image.png

我有一個解決方案,將 objCls 轉成一個 NSObject 即可,如下:

// 將 Class 轉成 NSObject
NSObject* clsObj = (NSObject*)objCls;
NSLog(@"%p", clsObj);

然后通過查詢 clsObjisa 的值,就是 objClsisa 的值。

判斷是否為元類的方法:

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 發現這樣的結果:

image.png

結果不一樣了!!!!
是的,是這樣的。在一些系統下,做了一個轉換,什么樣的轉換呢?先看一下結果:


image.png

厲害了,這是一個巧合吧,這個巧合不太巧合,是一個規律。在一些系統下的 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_MASKISA_MAGIC_VALUE

關于 LLDB 的更多使用,可以參考:Xcode 常用 LLDB 指令

謝謝!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容