為什麼 object_getClass(obj) 與 [OBJ class] 返回的指標不同

發表於2016-12-27
111490498-f2ae1c9f67391a5d

引言

該文章與runtime相關,開始並沒打算寫,因為大神們寫了好多runtime的文章,分析的都很全面、很深刻,再寫也就是班門弄斧。但還是寫了,因為我在看一個東西的時候偶爾發現了object_getClass(obj)與[OBJ class]返回的指標不同,感覺非常奇怪,因為它顛覆了我們對runtime中類結構模型的認識,後來在網上找了相關問題的答案,發現並沒有,所以打算寫一篇文章來和大家說說這個問題,我會講一點點runtime,但不會系統的講,目的就是為了能讓大家理解我說的問題就可以了。

runtime講哪些東西?

runtime是很寬泛的概念,通常我們在講runtime的時候大多側重以下兩方面:

1.基於Class、Object的結構模型講解。

2.實踐中基於runtime的api應用,這裡講的最多的就是基於method swizzling來實現AOP。

我遇到的問題就是與Class、Object的結構模型相沖突的,所以我們今天要討論的是前者。

runtime是開源的,大家要想了解細節還是要大概的看看原始碼

使用runtime的api要

先從runtime原始碼說起

我們先從runtime原始碼開始瞭解一些本質上的東西。

類的本質就是結構


Class的本質就是代表類的結構體的指標


id也是一個結構體指標


NSObject本質上也是結構


這些都是runtime中的原始碼,沒什麼好說的,拿出來就是要幫助大家加深對runtime的理解,下面來說說objc_class結構裡的isa和super_class之間的關係,然後深入理解一下Class,這與我遇到的問題是有關係的。其實runtime還有很多重要的概念,比如SEL、IMP、Method等…由於和我們今天討論的問題沒有關係,這裡不再展開說明了,這幾個概念都與訊息轉發關係密切,感興趣大家可以看原始碼。

runtime經典圖分析

121490498-58b297882c104b6c

相信瞭解runtime的朋友對這張圖不陌生,我們在這裡再看看這張圖說明了什麼:

1.橫向看:例項是物件,類也是物件(類物件),meta類也是物件(原類物件)

這是很重要的一點,希望大家理解,我們這裡忽略上下結構,先看左右結構,從左到右的指向就是之前介紹的runtime原始碼中objc_class結構裡isa的指向,Instance指的是我們建立的物件,Subclass(class)就是建立該物件的那個類,注意:建立物件的類本身也是物件,稱為類物件,類物件中存放的是描述例項相關的資訊,例如例項的成員變數,例項方法

類物件裡的isa指標指向Subclass(meta),Subclass(meta)也是一個物件,是原類物件,原類物件中存放的是描述類相關的資訊,例如類方法,在這一過程中,isa的兩次指向很像很像,大家注意理解。

類本身作為一個物件這件事情其實還是值得我們花時間來想想的,我當時是在想NSTimer自釋放問題的時候想到類物件的,從而才發現了本文討論的問題。

由於Subclass(meta)在橫向上已經沒有可以指向的物件了,所以他們的isa指標統一指向縱向(繼承關係)上的根meta class。而根meta class的isa則指向自己,我們後面會在程式碼中把這些結論性的東西驗證了。

2.縱向看:

superclass指標很容易理解,就是按照繼承關係向上指的,一直到繼承鏈的最上方,值得說的是Root class(class)的superclass指向是nil,Root class(meta)的superclass指向它的Root class (class),這個注意一下。

從程式碼上理解上面的圖

這裡介紹幾個runtime中的方法,還是看runtime原始碼:

object_getClass()就是順著isa的指向鏈找到對應的類,我們一會要驗證這個isa的指向鏈是否與上面圖中是一致的,就是用這個方法。

與之相關還有一個方法:object_setClass(),我們可以用該方法,簡單的來看一下runtime的強大,它可以動態改變類。首先要知道我們在NSLog的時候用的%@列印物件的時候其實是呼叫該類的description方法,而且我們還知道,NSArray物件的description會把每個array裡的元素都列印出來,而NSObject物件的description就僅僅列印類名和指標,下面通過一小段程式碼看看runtime的強大。

是不是很不可思議?runtime就是這麼強大!

這是NSObject類裡例項方法class與類方法class的實現,這裡再強調一下:類方法是在meta class裡的,類方法就是把自己返回,而例項方法中是返回例項isa的類,我們要驗證這個isa的指向鏈的時候不能用這種方法,千萬記住,為什麼,一會說明。

看程式碼:

Log輸出:

分析:
注:Person是一個繼承自NSObject的普通類,裡面有個name屬性。

1.我們發現呼叫class方法的方式不能得到isa的指向鏈,但是第一次呼叫是正確的(class的輸出都是0x10ae0e178),為什麼?原因就是上面貼出來的class原始碼中,我們第一次呼叫的class是例項方法,會返回isa的類,但是第二次開始呼叫的就是類方法,返回的是本身,所以還是0x10ae0e178,以後無論怎麼呼叫都是執行的類方法,返回的都是本身,所以,用class方法是得不到isa指向鏈的。

2.用object_getClass()驗證了我們Class、Object結構模型理論是對的,我們這裡特意的列印了root meta class 的isa,發現果然指向是自己(0x10b66a198)。

3.從列印結果我們能看到,類也是物件,meta類也是物件,都佔有一塊記憶體,而且我們會發現類物件、meta類物件、root meta類物件的指標都是用9位16進位制數表示,而例項物件是用12位16進位制數表示(這裡用的是64位模擬器),為什麼這些類物件的指標位數少?因為它們存在於段上,並不在棧或者堆上,黑魔法那篇文章說過段記憶體的事情。也就是說可以把這些類物件理解成單利,這是很重要的一點,希望大家理解,這一點可以讓我們天馬行空的想很多,比如可不可以把網路請求寫在類物件裡,嫩不能用類物件去解決自釋放的問題,等等…這會是很有意思的思考。

我們理解了這個結構模型之後,看看我遇到的問題吧。

問題來了

看程式碼:

Log輸出:

問題來了:
為什麼[NSTimer class]:0x10ecdfe38與class:0x10ece02c0得到的指標不一樣?

就是說為什麼object_getClass(obj)與[OBJ class]返回的指標不同?

[NSTimer class]返回應該是類物件,object_getClass(timer1)返回的也應該是類物件,上面也說過,可以把類物件理解成單利,為什麼指標不同?

如果用Person類做實驗兩者返回就是相同的,如果用系統其它類做實驗兩者返回還是不同的,它們本身之間就有矛盾,更重要的是,與我們剛剛理解的結構模型也是矛盾的,如何用這個模型理論去解釋[NSTimer class]返回的這個指標?

感興趣的朋友可以不往下看,自己想想為什麼,其實很簡單,但是沒想到會是這樣的,我當時就是這個感受。


131490498-1051e34eb1098322
面朝大海,春暖花開

答案來了

答案非常簡單,兩個字:類簇

看程式碼:

程式碼沒有變,只是我們這次不列印指標,列印物件的描述:

Log輸出:

我們發現我們之前結構模型的認識沒有錯,之所以矛盾是因為NSTimer是個類簇,它返回的並不是NSTimer物件,而是__NSCFTimer物件!我沒有想到NSTimer也是類簇,我們熟悉的類簇是NSNumber,NSArray,NSDictionary、NSString…這說明大多數的OC類都是類簇實現的(連NSTimer也不放過),也說明為什麼我們Person類是正常的,因為它不是類簇實現的。

這裡還是簡單的說說類簇的概念吧:一個父類有好多子類,父類在返回自身物件的時候,向外界隱藏各種細節,根據不同的需要返回的其實是不同的子類物件,這其實就是抽象類工廠的實現思路,iOS最典型的就是NSNumber。

這裡的numberWithXXXX方法是類工廠返回的其實並不是NSNumber類,而是各個子類,NSCFNumber、NSCFBoolean。

這和我們上面問題中的NSTimer很像,類方法
scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:並沒有返回NSTimer物件,而是返回了它的子類__NSCFTimer物件。

有人可能要問,如何證明__NSCFTimer就是NSTimer的子類,如何證明類簇是真的?其實很簡單:

總結

我把這個問題記錄下來就是希望這篇文章對恰好遇到這個問題的朋友、有相同困惑的朋友一些幫助,如果是NSArray遇到了相同的問題我可能立馬想到類簇,因為它還有可變的物件,但是NSTimer…

我猜中了開頭,可我猜不著這結局。——紫霞仙子《大話西遊》

我猜到了結局,但猜不到原因,誰讓iOS是封閉的系統呢?

歡迎大家和我交流溝通,文章中有任何錯誤和漏洞,懇請指正,謝謝。

相關文章