談一談OC運行時及消息轉(zhuǎn)發(fā)

運行時

網(wǎng)上對運行時機制有很多籠統(tǒng)的說法,但相信還是會有很多人并不完全理解運行時的機制。那么什么是運行時呢?
先來看一種寫法:

@interface MyClass : NSObject 
{
    @public
        NSString *_myName;
    @private
        NSString *_myID;
}

OC中是支持public和private關鍵字的,類似于java或C#,但是我們在編寫OC代碼的時候卻很少這么做。因為使用這種寫法,對象布局在編譯器就已經(jīng)固定了。只要碰到訪問_myName變量的代碼,編譯器就把其替換為偏移量(offset),這個偏移量是硬編碼,表示該變量距離存放對象的內(nèi)存區(qū)域的起始地址有多遠。但是如果在運行過程中,又新增了一個實例變量,硬編碼于其中的變量就會讀到錯誤的值,例如我們假設每個變量的指針都是4個字節(jié),新增一個變量myGender:

@interface MyClass : NSObject 
{
    @public
        NSString *_MyGender;(offset為0)
        NSString *_myName;(offset為+4)
    @private
        NSString *_myID;(offset為+8)
}

如果offset采用硬編碼,原類中offset為0是應該訪問到_myName現(xiàn)在卻訪問到_myGender。
OC的做法是,把實例變量當做一種存儲offset所用的特殊變量,交由類對象保管。offset會在運行期查找,如果類的定義變了,那么offset也會相應改變,這樣的話,無論何時訪問實例變量,總能訪問到正確的偏移位置。
這就是我們所說的運行時機制。想更深入的了解運行時機制可以看看這篇文章:
http://quotation.github.io/objc/2015/05/21/objc-runtime-ivar-access.html
這篇文章同時解釋了為什么OC無法動態(tài)添加成員變量。

消息轉(zhuǎn)發(fā)

再來說一說消息轉(zhuǎn)發(fā),消息轉(zhuǎn)發(fā)是運行時機制的一大特點。在了解消息轉(zhuǎn)發(fā)之前先來了解一個OC的底層函數(shù):

void objc_msgSend(id self, SEL cmd, ...)

在OC中,所有的方法最終都會轉(zhuǎn)換為普通的C語言函數(shù),例如一個對象object的方法:

- (id)doSomething:(id)parameter
{
    //doSomething
}

給object發(fā)送消息:

id returnValue = [object doSomething:patameter];

object是接受者,doSomething叫做選擇子,選擇子與參數(shù)合起來叫做消息,編譯器看到此消息后,會將其轉(zhuǎn)換為一條標準的C語言函數(shù),就是最開始的那個函數(shù),它是消息傳遞機制中的核心函數(shù),上述消息會轉(zhuǎn)換為:

id returnValue = objc_msgSend(object,@selector(doSomething:),parameter);

objc_msgSend函數(shù)會根據(jù)接受者與選擇子的類型來調(diào)用適當?shù)姆椒ā榱送瓿纱瞬僮?,該方法需要在接受者所屬的類中搜尋方法列表,如果找到了名稱相符的方法,就跳轉(zhuǎn)至其實現(xiàn)代碼,如果找不到,就沿著繼承體系向上查找。所過最終依然沒有找到相符的方法,就會執(zhí)行消息轉(zhuǎn)發(fā)。
一個完整的消息轉(zhuǎn)發(fā)過程會經(jīng)歷三個階段:

  • 動態(tài)方法解析(resolveInstanceMethod或resolveClassMethod)
  • 備選接收者(forwardingTargetForSelecor)
  • 完整消息轉(zhuǎn)發(fā)(forwardInvocation)

</br>

1. 動態(tài)方法解析

在消息轉(zhuǎn)發(fā)開始時,本類有機會新增一個處理選擇子的方法,如果選擇子是實例方法會調(diào)用:

+ (BOOL)resolveInstanceMethod : (SEL)selector

如果是類方法,則會調(diào)用:

+ (BOOL)resolveClassMethod : (SEL)selector

例如,用此方案來實現(xiàn)@dynamic屬性:

id autoDictionaryGetter(id self, SEL _cmd);
id autoDictionarySetter(id self, SEL _cmd, id value);
 
+ (BOOL)resolveInstanceMethod:(SEL)selector
{
    NSString *selectorString = NSStringFromSelector(selector);
    if (/*選擇子是個@dynamic屬性*/)
        if ([selectorString hasPrefix:@"set"]) {
            class_addMethod(self, selector, (IMP)autoDictionarySetter, "v@:@");
        } else {
            class_addMethod(self, selector, (IMP)autoDictionaryGetter, "@@:");
        }
        return YES;
    }
    return [super resolveInstanceMethod:selector];
}

如果前綴為set,就是set方法,否則是get方法。不管哪種情況,都會把處理該選擇子的方法動態(tài)的加到類里面,以最在開始處理消息轉(zhuǎn)發(fā)。例子中用到了IMP指針,想了解IMP指針可以看看這篇博客:
http://m.itdecent.cn/p/425a39d43d16?utm_campaign=maleskine&utm_content=note&utm_medium=writer_share&utm_source=weibo

2. 備選接收者

如果在動態(tài)方法解析時沒有處理消息轉(zhuǎn)發(fā),那么還有第二次機會來處理未知的選擇子,在這一步中系統(tǒng)會詢問:有沒有其他接收者來處理這條消息?該步驟對應的處理方法如下:

- (id)forwardingTargetForSelector:(SEL)selector

但是我們無法操作經(jīng)由這一步所轉(zhuǎn)發(fā)的消息,如果想在發(fā)送給備選接受者之前先修改消息內(nèi)容,就得通過完整的消息轉(zhuǎn)發(fā)機制來做了。

3. 完整的消息轉(zhuǎn)發(fā)

首先,創(chuàng)建NSInvacation對象,把與尚未處理的那條消息有關的全部細節(jié)都封裝在其中,此對象包括選擇子,target及參數(shù)。在觸發(fā)NSInvocation對象時,消息轉(zhuǎn)發(fā)系統(tǒng)將親自出馬,把消息指派給目標對象。
此過程會調(diào)用:

- (void)forwardInvocation:(NSInvocation *)invocation

這個方法可以實現(xiàn)的很簡單:只要改變調(diào)用target,使消息在新targer上得以調(diào)用即可。然而這樣的實現(xiàn)就與備選接受者的實現(xiàn)方法等效了。一般的做法是:在觸發(fā)消息前,先以某種方式改變消息內(nèi)容,比如追加另一個參數(shù),或更改選擇子等等。
實現(xiàn)此方法時,若發(fā)現(xiàn)調(diào)用操作不應由本類處理,則需調(diào)用超類的同名方法。這樣的話,繼承體系中的每個類都有機會處理此調(diào)用請求,直至NSObject。如果最終此消息未得到處理,則會調(diào)用NSObject的doesNotRecognizeSelector:,以拋出異常。
完整的消息轉(zhuǎn)發(fā)用到NSInvocation對象,想了解NSInvocation可以看一看這篇博文:
http://mp.weixin.qq.com/s?__biz=MjM5NTIyNTUyMQ==&mid=208927760&idx=1&sn=30b9caecba709553e463d719668454ae&scene=2&from=timeline&isappinstalled=0#rd

4. 完整的消息轉(zhuǎn)發(fā)流程圖

另外需要注意的是,消息轉(zhuǎn)發(fā)過程中,步驟越往后,處理消息的代價就越大,最好能在第一步就處理完,這樣的話,運行期系統(tǒng)可以將此方法緩存。如果這個類的實例還會再接收到同名選擇子,那么根本無須再次啟動消息轉(zhuǎn)發(fā)流程。
另外一篇介紹運行時機制的博文:
http://www.cocoachina.com/ios/20150715/12540.html

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 2,106評論 0 9
  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,715評論 19 139
  • 參考鏈接: http://www.cnblogs.com/ioshe/p/5489086.html 簡介 Runt...
    樂樂的簡書閱讀 2,252評論 0 9
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,366評論 25 708
  • 一 我是個和尚,我沒有名字。 小時候,別人叫我江流,因為我是江里飄來的,后來我剃了度,法號叫玄奘,從此我沒有名字。...
    江都浪子閱讀 937評論 2 51

友情鏈接更多精彩內(nèi)容