OC消息發送機制完整全過程

前言

時間荏苒,光陰似箭啊,不知不覺,自己從接觸iOS開發至今已經六年有余了。想想最開始學習OC時,蘋果還未推出ARC機制,iOS/Mac開發也只有OC這么一門語言,swift還未推出。仔細想想自己從接觸到工作這么多年來,自己接觸到的OC語言相關問題的坑還是蠻多的,今天就讓我給大家好好的講講iOS面試中甚至開發中經常接觸到的一個東西:消息發送機制

正文

我從學習編程開發到現在也是學過和接觸過很多編程語言了,大家熟知的匯編/C/C++/swift,我都一一認真的學過,因此對編程語言也是略知一二。不過呢,在這些常見的編程語言中,OC也算是比較獨特的一門語言了,尤其是它的動態性,當然還有它那反人類的方法調用語法-中括號。說到方法調用,就不得不提到的就是OC里的另一個獨特點,那就是消息發送機制,這也可以說是在其他大部分編程語言中沒有的東西。下面就讓我開始表演吧。

大部分語言的方法調用

說起大部分的語言的方法調用這個東西,可能就有點籠統了,確切的說呢,方法調用其實都是這一個樣子的,哪個樣子的呢,就是無論啥語言最終轉成的匯編代碼里,方法調用都是一個樣子,那就是直接或間接的call某個地址,而這里的某個地址,就是函數實現的地址,直白的說就是那個函數實現的那段代碼的入口首地址。

類對象、元類對象

其實呢,OC的方法調用最終也是上面這種形式的,只是呢,它在進入最終的這個調用形式的之前,四處瞎轉悠了一段時間,轉悠完后才進入這最終步驟的。而他這四處轉悠那段時間干的事情就是消息發送了。

說起OC的消息發送就不得不說起的一個東西,那就是類對象和元類對象了。

不知道大家是否曾想過這樣一個問題,一個對象的類信息是存儲在哪兒的?所謂的類信息,就是類的屬性名、方法等這些東西。其實可能有挺多小伙伴應該知道一個東西,那就是,一個對象的內存布局,幾乎大部分面相對象編程語言中,一個對象具體的屬性值(例如,a = 10,那么10就是這個具體的屬性值)是直接存儲在相應的對象的內存布局中的。而對象的屬性名、方法這些東西是不會存儲在一個對象的內存布局中的,原因其實很簡單的。首先我們來思考一個問題,為何一個對象要將具體的屬性值存儲在自己的內存布局中呢?如果大家仔細思考過這個問題的話,會發現,不同的一個實例對象,在內存中都有獨一無二的一塊內存存儲著它,這就說明一個問題,不同的內存中存儲著不同的一個對象,也就是,某一塊內存中存儲著的對象都是不同于其它任何內存中存儲的對象。既然如此,那就意味著不同的對象的各個屬性值都應該是自己獨有的,自己可以進行相應的修改,并且改了不會影響其它任何內存中存儲著的實例對象??偨Y一句話就是,因為每個對象都要擁有自己的各個屬性值,因此最好的方式就是在自己的內存布局之中存放這些具體的屬性值(P.S. 其實這里也給大家解釋了OC里另一個知識點,那就是,為何不能在分類中添加成員變量了,原因就是添加成員變量會直接影響到一個對象的內存布局,所以并不是蘋果沒寫分類中添加成員變量這樣的功能,而是他們壓根實現不了這功能,就是因為載進內存里的程序的內存布局是不允許修改的,也不可能修改的)。那么,想必大家對屬性名、方法這些東西為何不存儲在對象的內存布局中有些端倪了吧,就像上面說的,原因很簡單,那就是一個類的任何對象的屬性名、方法這些東西都是一樣的,既然都是一樣的,那么又何必大費周章的在每個實例對象中都存儲一遍呢?因此,蘋果就將這些都一樣的東西放在了另一個東西里面了,那就是這里要提到的類對象和元類對象。類對象里存儲著一個類的實例屬性、實例方法等,元類對象存儲著一個類的類方法等。其實呢,實例屬性、實例方法、類方法這些東西可以放在同一個東西里面,蘋果為何分成兩個東西來存儲,我想應該是為了方便管理和區分。這里其實也可以知道一點,那就是,一個程序中,一個類的實例對象可以有無數個,但是這些實例對象對應著的類對象和元類對象有且僅有一個,原因想必大家一目了然,就是因為一個程序中,一個類有且只有一個,那么就自然而然只有一個類對象和元類對象來存儲這些類信息。

這里給上面做個大體的總結:一個類的類信息(也就是屬性和方法這些一個類都共同具有的東西)是需要一個東西來存儲的,那就是類對象和元類對象,類對象存儲一個類的實例方法、實例屬性之類的實例相關的東西,元類對象是用來存儲一個類的類方法等類性質相關的東西的。而一個對象的具體數值都是存儲在自己的內存布局中的,原因就是每個對象都要獨立擁有自己的屬性值,可以的對這些屬性值進行修改并不影響到其他對象。

類對象和元類對象中存儲的東西

類對象和元類對象中存儲的東西很多,我這里就不一一贅述了,我在這里就只說我們這篇文章應該用到的東西。首先,就是大家耳熟能詳的一個東西isa指針,isa指針是實例對象、類對象、元類對象都有的一個東西。實例對象的isa指向著類對象,而這個類對象中存儲著類實例對象的相關信息,可以告訴實例對象,你是個什么類,你有哪些屬性、方法,遵守了哪些協議等。類對象的isa指針指向了元類對象,元類對象中存儲著一個類的類方法等類性質相關的東西。元類的isa指向NSObject的元類對象。如下圖

而類對象和元類對象中都存儲著一個superclasss,用于存儲自己相應的父類,并可以通過這個superclass查找到自己的父類。例如Animal繼承自NSObject,Dog繼承自Animal,那么指向關系就是,Dog的實例對象的isa指向Dog的類對象,Dog的類對象的isa指向Dog的元類對象。Dog的類對象的superclass指向著Animal的類對象,Dog的元類對象的superclass指向Animal的元類對象。類對象的superclass一層一層往上指向父類的類對象,直到NSObject的類對象,NSObject的類對象的superclass指向nil。元類對象的superclass一層一層往上指向父類的元類對象,直到NSObject的元類對象,NSObject的元類對象的superclass指向NSObject的類對象。關系圖如下,這里的實線箭頭是superclass,虛線箭頭是isa

消息發送機制

消息發送機制總結起來分為三個大的過程:

1、消息發送過程(也就是方法查找過程)

2、動態方法解析過程

3、消息轉發過程

1、消息發送過程

首先得說一個前提條件,那就是,如果能用中括號調用方法的話,那么這個方法一定是有聲明的,這個可能有些小伙伴并不會注意到這一點,那就是,正常情況下,一個方法都是有聲明和實現的,聲明就是告訴你有這個方法存在,實現就是這個方法的具體實現,

說起消息發送過程,就得說說,在類對象和元類對象中,有兩個東西,一個是方法緩存列表,也即是cache,這是個散列表數據結構(也就是大家耳熟能詳的哈希表),它主要用于緩存自己之前調用過的方法。另一個是方法列表,這里面存儲著一個類的所有方法,包括分類添加的方法。注意:類對象和元類對象都有這兩個東西,只是一個存儲著實例方法,一個存儲著類方法。

下面就是消息發送的詳細過程(這里我們只說實例方法,類方法是一樣的,只是最終在元類對象中查找的而已):

首先,這也是能解決小伙伴們很多時候遇到的某些莫名問題的原因,那就是,在消息發送的最最開始的地方,會檢查你調用的方法的對象(也就是self)是不是一個nil,如果是的話,就直接停止后面所有的操作,原因很簡單,你都穿了個空對象過來,后面的所有操作其實都是沒有任何意義的啦。緊接著,就會通過實例對象的isa指針找到其類對象,之后再類對象的cache中進行查找,如果找到,就直接調用,之后結束查找。

如果在cache中沒找到,則在這個類對象的方法列表中進行查找,如果找到,則調用,并將此方法存放在自己的cache中,之后結束查找。

如果在類對象中未找到,怎通過類對象的superclass找到父類的類對象,之后重復前兩步(也就是先在cache中查找,之后再在方法列表中查找)。superclass為nil,如果superclass為nil,則進入下面的動態方法解析階段。注意:這里找到之后會將找到的方法放在自己的cache中,而不是父類或者父類的父類的cache中,并且,即使是在父類的cache中找到的話,也會放在自己的cashe中的。這樣的原因很簡單:盡量的減少方法查找的流程,一步到位,這樣就可以節省很多時間

具體如下圖:


消息發送過程圖

2、動態方法解析階段

進入這里之前,首先會判斷是否進入過這個動態方法解析階段,如果進入過則直接進入下一步:方法轉發階段。如果沒有進入過,就會進入這個方法解析階段,方法解析會調用一個法法,叫做- resolveInstanceMethond: ,類方法的動態方法解析階段的話就叫做+ resolveInstanceMethond:,我們前面已經說過了,一個方法能用中括號進行調用的話,那么這個方法必須至少有個聲明,但是呢,如果能來到這兒,也就證明了一點,這個方法只有聲明,沒有實現,而這個動態方法解析的resolveInstanceMethond: 方法,就是給你調用的這個方法添加方法實現的,也就是具體的方法實現代碼。

當進入動態方法解析時,會調用resolveInstanceMethond:這個方法,執行方法里的代碼,而正常的操作都是會在這個方法里進行動態添加方法實現代碼,怎么添加,大家可以自己搜索。之后將這個動態方法解析階段標記為已經執行過的狀態。最后重新回到第一階段的消息發送階段----也就是在cache和方法列表中查找方法,為何要重新進入第一階段呢,原因很簡單,因為你已經為這個方法添加了實現,說白了就是將方法的實現添加進了那個相應的類的方法列表里了,如果你已經在動態方法解析階段添加了方法實現的話,那么重新進入第一階段的時候,就能夠在那個類的方法列表中找到那個方法的實現了。如果你沒有添加方法實現的話,重新進入第一階段的時候,就找不到方法實現,那么再次重新進入動態方法解析階段的時候,由于已經標記為執行過動態方法解析了,就不會再進入動態方法解析,就會直接進入下一個階段,那就是最后一個階段,消息轉發階段

具體如下圖:


動態方法解析過程圖

3、消息轉發階段

消息轉發,可謂是高級iOS面試問得最多的問題之一了吧,然鵝實際開發應用場景卻是不多的。

進入消息轉發階段后,會首先調用這個方法forwardingTargetForSelector:,這個方法會返回一個實例對象值,如果返回的是nil,則進入下個步驟,調用methodSignatureForSelector:。如果返回的是一個具體的實例對象,那么就會直接在這個實例對象的cache和方法列表中查找這個方法,找到的話就調用,沒找到的話,就方法找不到的那個錯誤。注意,這里調用返回的這個實例對象的方法名字和最開始查找的那個方法名字是一毛一樣的。

進入methodSignatureForSelector:,這個方法調用完,會返回一個methodSignature,如果返回的是空,則直接報方法找不到的錯,如果返回的不為空,則進入forwardInvocation:這個方法,注意,到這兒了, 就會直接調用這個方法,言下之意就是,你在這個方法里寫了什么代碼,就會一五一十的執行里面的代碼,所以愛搞事的其實就可以在這里面寫很多搞事的代碼啦,哈哈哈。當然,常規操作是,會根據methodSignatureForSelector:方法返回的methodSignature,之后調用這個方法簽名指向的方法實現。請注意:由于這里是直接調用這個forwardInvocation:方法,因此,其實也可以直接不管methodSignatureForSelector:返回值的東西,只要你明確知道自己想干什么操作,就可以直接在這forwardInvocation:方法里把你想寫的操作寫進去。

具體如下圖:


消息轉發過程圖

總結

到此為止,完整的消息發送機制就已經講完了。希望能對你的工作和面試有所幫助。


附加知識NSProxy

其實有時候,可能會存在這樣一種需求,那就是,不執行消息發送的第一階段和第二階段,能夠直接一上來就直接進入消息轉發階段。這個其實蘋果早已為我們想到了,那就是你自定義一個類繼承自NSProxy,這個類和NSObject一樣,是屬于OC少有的基類,通過這個類,很特殊,特殊在哪兒呢,你會發現,生成這個類的一個對象時,只有一個alloc方法,并沒有init方法,是不是感覺刷新了你對OC語言的認知新高度呢?當然它還有一個特殊點,就在于,一旦你實現了第三階段的三個方法的時候,調用方法時,就會略過第一階段和第二階段,直接進入第三階段。這樣的話,就給消息轉發提供了很大的一個捷徑。

大家可能會納悶,蘋果怎么要專門搞這個一個類呢,而且主要作用還是這樣的。這個原因的話,就得說到多繼承了。大家都知道,現在開發中流行的很多開發語言都是單繼承語言,就是因為多繼承容易出現一系列麻煩的問題,一個是菱形繼承的問題,一個是繼承邏輯的問題,注意,多繼承下是很容易出現這兩種情況的,尤其是繼承邏輯問題。這也是為啥如今很多面向對象語言都是單繼承的原因。然鵝呢,多繼承也是有它的優勢之處的,例如本來某各類就同時擁有另外兩個類的特性,那樣的話,同時繼承自這兩個類是一種很好的方式。

因此,大部分單繼承語言中,為了能保留多繼承的優勢,就另辟蹊徑的實現了多繼承,OC中當然也有這個另辟蹊徑的方法,那其中一種方法就是這里提到的消息轉發。大家可以想想,為啥第三階段的名字非得叫做消息轉發呢,聽名字就能讓你一下子明白這個階段的主要作用了,那就是將本身類沒有實現的方法轉發到另一個類的方法實現上。舉個例子,類A有test1和test2的方法實現,類B有test3和test4的實現,并且類A和B并無任何的繼承關系,并且A已經有一個父類了,那么現在想在類A中聲明test3和test4,并且實現的方法功能也是和類B的test3和test4一毛一樣,并且能直接通過類A的對象直接調用,那么,最好的方法就是直接調用類B的這兩個方法,那么這不就是多繼承了嗎。而這個例子,能夠用消息轉發輕易的實現,而NSProxy剛好又是那個一步到位直接進入消息轉發階段的東西,因此這就是NSProxy出現的主要原因之一。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容