運行時
網(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