Runtime --- 消息發(fā)送

上篇內(nèi)容我們主要了解了objc_msgSend方法的幾個參數(shù)和objc_class的結構
本篇內(nèi)容我們一起了解 消息發(fā)送

(一)消息

Objc 中發(fā)送消息是用中括號 [] 把接收者和消息括起來,而直到運行時才會把消息與方法實現(xiàn)綁定。

objc_msgSend函數(shù)

在上篇文章Runtime 淺談中已經(jīng)對objc_msgSend進行了一點介紹,看起來像是objc_msgSend返回了數(shù)據(jù),其實objc_msgSend從不返回數(shù)據(jù)而是你的方法被調(diào)用后返回了數(shù)據(jù)。下面詳細敘述下消息發(fā)送步驟:

  1. 檢測這個 selector 是不是要忽略的。比如 Mac OS X 開發(fā),有了垃圾回收就不理會 retain,release 這些函數(shù)了。
  2. 檢測這個 target 是不是 nil 對象。ObjC 的特性是允許對一個 nil 對象執(zhí)行任何一個方法不會 Crash,因為會被忽略掉。
  3. 如果上面兩個都過了,那就開始查找這個類的 IMP,先從 cache 里面找,完了找得到就跳到對應的函數(shù)去執(zhí)行。
  4. 如果 cache 找不到就找一下方法分發(fā)表。
  5. 如果分發(fā)表找不到就到超類的分發(fā)表去找,一直找,直到找到NSObject類為止。
  6. 如果還找不到就要開始進入動態(tài)方法解析了,后面會提到。
PS:這里說的分發(fā)表其實就是Class中的方法列表,它將方法選擇器和方法實現(xiàn)地質(zhì)聯(lián)系起來

其實編譯器會根據(jù)情況在objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, 或objc_msgSendSuper_stret四個方法中選擇一個來調(diào)用。如果消息是傳遞給超類,那么會調(diào)用名字帶有”Super”的函數(shù);如果消息返回值是數(shù)據(jù)結構而不是簡單值時,那么會調(diào)用名字帶有”stret”的函數(shù)。排列組合正好四個方法。

方法中的隱藏參數(shù)

我們經(jīng)常在方法中使用self關鍵字來引用實例本身,但從沒有想過為什么self就能取到調(diào)用當前方法的對象吧。其實self的內(nèi)容是在方法運行時被偷偷的動態(tài)傳入的。

objc_msgSend找到方法對應的實現(xiàn)時,它將直接調(diào)用該方法實現(xiàn),并將消息中所有的參數(shù)都傳遞給方法實現(xiàn),同時,它還將傳遞兩個隱藏的參數(shù):
– 接收消息的對象(也就是self指向的內(nèi)容) – 方法選擇器(_cmd指向的內(nèi)容) 之所以說它們是隱藏的是因為在源代碼方法的定義中并沒有聲明這兩個參數(shù)。它們是在代碼被編譯時被插入實現(xiàn)中的。盡管這些參數(shù)沒有被明確聲明,在源代碼中我們?nèi)匀豢梢砸盟鼈儭T谙旅娴睦又?,self引用了接收者對象,而_cmd引用了方法本身的選擇器:

- strange
{
    id  target = getTheReceiver();
    SEL method = getTheMethod();
 
    if ( target == self || method == _cmd )
        return nil;
    return [target performSelector:method];
}

在這兩個參數(shù)中,self 更有用。實際上,它是在方法實現(xiàn)中訪問消息接收者對象的實例變量的途徑。
而當方法中的super關鍵字接收到消息時,編譯器會創(chuàng)建一個objc_super結構體:

struct objc_super { id receiver; Class class; };

這個結構體指明了消息應該被傳遞給特定超類的定義。但receiver仍然是self本身,這點需要注意,因為當我們想通過[super class]獲取超類時,編譯器只是將指向selfid指針和classSEL傳遞給了objc_msgSendSuper函數(shù),因為只有在NSObject類找到class方法,然后class方法調(diào)用object_getClass(),接著調(diào)用objc_msgSend(objc_super->receiver, @selector(class)),傳入的第一個參數(shù)是指向selfid指針,與調(diào)用[self class]相同,所以我們得到的永遠都是self的類型。

獲取方法地址

在IMP那節(jié)提到過可以避開消息綁定而直接獲取方法的地址并調(diào)用方法。這種做法很少用,除非是需要持續(xù)大量重復調(diào)用某方法的極端情況,避開消息發(fā)送泛濫而直接調(diào)用該方法會更高效。

NSObject類中有個methodForSelector:實例方法,你可以用它來獲取某個方法選擇器對應的IMP,舉個例子:

void (*setter)(id, SEL, BOOL);
int I;

setter = (void (*)(id, SEL, BOOL))[target
    methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
    setter(targetList[i], @selector(setFilled:), YES);

當方法被當做函數(shù)調(diào)用時,上節(jié)提到的兩個隱藏參數(shù)就需要我們明確給出了。上面的例子調(diào)用了1000次函數(shù),你可以試試直接給target發(fā)送1000次setFilled:消息會花多久。

PS:methodForSelector:方法是由 Cocoa 的 Runtime 系統(tǒng)提供的,而不是 Objc 自身的特性。

動態(tài)方法解析

你可以動態(tài)地提供一個方法的實現(xiàn)。例如我們可以用@dynamic關鍵字在類的實現(xiàn)文件中修飾一個屬性:

@dynamic propertyName;

這表明我們會為這個屬性動態(tài)提供存取方法,也就是說編譯器不會再默認為我們生成setPropertyName:和propertyName方法,而需要我們動態(tài)提供。我們可以通過分別重載resolveInstanceMethod:和resolveClassMethod:方法分別添加實例方法實現(xiàn)和類方法實現(xiàn)。因為當 Runtime 系統(tǒng)在Cache和方法分發(fā)表中(包括超類)找不到要執(zhí)行的方法時,Runtime會調(diào)用resolveInstanceMethod:或resolveClassMethod:來給程序員一次動態(tài)添加方法實現(xiàn)的機會。我們需要用class_addMethod函數(shù)完成向特定類添加特定方法實現(xiàn)的操作:

void dynamicMethodIMP(id self, SEL _cmd) {
    // implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(resolveThisMethodDynamically)) {
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
          return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}
@end

上面的例子為resolveThisMethodDynamically方法添加了實現(xiàn)內(nèi)容,也就是dynamicMethodIMP方法中的代碼。其中 “v@:” 表示返回值和參數(shù),這個符號涉及 Type Encoding

PS:動態(tài)方法解析會在消息轉發(fā)機制浸入前執(zhí)行。如果 respondsToSelector: 或instancesRespondToSelector:方法被執(zhí)行,動態(tài)方法解析器將會被首先給予一個提供該方法選擇器對應的IMP的機會。如果你想讓該方法選擇器被傳送到轉發(fā)機制,那么就讓resolveInstanceMethod:返回NO。

(二)消息轉發(fā)

重定向

在消息轉發(fā)機制執(zhí)行前,Runtime 系統(tǒng)會再給我們一次偷梁換柱的機會,即通過重載- (id)forwardingTargetForSelector:(SEL)aSelector方法替換消息的接受者為其他對象:

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if(aSelector == @selector(mysteriousMethod:)){
        return alternateObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

畢竟消息轉發(fā)要耗費更多時間,抓住這次機會將消息重定向給別人是個不錯的選擇,不過千萬別返回self,因為那樣會死循環(huán)。

轉發(fā)

當動態(tài)方法解析不作處理返回NO時,消息轉發(fā)機制會被觸發(fā)。在這時forwardInvocation:方法會被執(zhí)行,我們可以重寫這個方法來定義我們的轉發(fā)邏輯:

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:
            [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
}

該消息的唯一參數(shù)是個NSInvocation類型的對象——該對象封裝了原始的消息和消息的參數(shù)。我們可以實現(xiàn)forwardInvocation:方法來對不能處理的消息做一些默認的處理,也可以將消息轉發(fā)給其他對象來處理,而不拋出錯誤。

這里需要注意的是參數(shù)anInvocation是從哪的來的呢?其實在forwardInvocation:消息發(fā)送前,Runtime系統(tǒng)會向對象發(fā)送methodSignatureForSelector:消息,并取到返回的方法簽名用于生成NSInvocation對象。所以我們在重寫forwardInvocation:的同時也要重寫methodSignatureForSelector:方法,否則會拋異常。

當一個對象由于沒有相應的方法實現(xiàn)而無法響應某消息時,運行時系統(tǒng)將通過forwardInvocation:消息通知該對象。每個對象都從NSObject類中繼承了forwardInvocation:方法。然而,NSObject中的方法實現(xiàn)只是簡單地調(diào)用了doesNotRecognizeSelector:。通過實現(xiàn)我們自己的forwardInvocation:方法,我們可以在該方法實現(xiàn)中將消息轉發(fā)給其它對象。

forwardInvocation:方法就像一個不能識別的消息的分發(fā)中心,將這些消息轉發(fā)給不同接收對象。或者它也可以象一個運輸站將所有的消息都發(fā)送給同一個接收對象。它可以將一個消息翻譯成另外一個消息,或者簡單的”吃掉“某些消息,因此沒有響應也沒有錯誤。forwardInvocation:方法也可以對不同的消息提供同樣的響應,這一切都取決于方法的具體實現(xiàn)。該方法所提供是將不同的對象鏈接到消息鏈的能力。

注意: forwardInvocation:方法只有在消息接收對象中無法正常響應消息時才會被調(diào)用。 所以,如果我們希望一個對象將negotiate消息轉發(fā)給其它對象,則這個對象不能有negotiate方法。否則,forwardInvocation:將不可能會被調(diào)用。

轉發(fā)和多繼承

轉發(fā)和繼承相似,可以用于為Objc編程添加一些多繼承的效果。就像下圖那樣,一個對象把消息轉發(fā)出去,就好似它把另一個對象中的方法借過來或是“繼承”過來一樣。


image

這使得不同繼承體系分支下的兩個類可以“繼承”對方的方法,在上圖中Warrior和Diplomat沒有繼承關系,但是Warrior將negotiate消息轉發(fā)給了Diplomat后,就好似Diplomat是Warrior的超類一樣。

消息轉發(fā)彌補了 Objc 不支持多繼承的性質(zhì),也避免了因為多繼承導致單個類變得臃腫復雜。它將問題分解得很細,只針對想要借鑒的方法才轉發(fā),而且轉發(fā)機制是透明的。

替代者對象(Surrogate Objects)

轉發(fā)不僅能模擬多繼承,也能使輕量級對象代表重量級對象。弱小的女人背后是強大的男人,畢竟女人遇到難題都把它們轉發(fā)給男人來做了。這里有一些適用案例,可以參看官方文檔。

轉發(fā)與繼承

盡管轉發(fā)很像繼承,但是NSObject類不會將兩者混淆。像respondsToSelector: 和 isKindOfClass:這類方法只會考慮繼承體系,不會考慮轉發(fā)鏈。比如上圖中一個Warrior對象如果被問到是否能響應negotiate消息:

if ( [aWarrior respondsToSelector:@selector(negotiate)] )
    ...

結果是NO,盡管它能夠接受negotiate消息而不報錯,因為它靠轉發(fā)消息給Diplomat類來響應消息。

如果你為了某些意圖偏要“弄虛作假”讓別人以為Warrior繼承到了Diplomat的negotiate方法,你得重新實現(xiàn)respondsToSelector:isKindOfClass:來加入你的轉發(fā)算法:

- (BOOL)respondsToSelector:(SEL)aSelector
{
    if ( [super respondsToSelector:aSelector] )
        return YES;
    else {
        /* Here, test whether the aSelector message can     *
         * be forwarded to another object and whether that  *
         * object can respond to it. Return YES if it can.  */
    }
    return NO;
}

除了respondsToSelector:isKindOfClass:之外,instancesRespondToSelector:中也應該寫一份轉發(fā)算法。如果使用了協(xié)議,conformsToProtocol:同樣也要加入到這一行列中。類似地,如果一個對象轉發(fā)它接受的任何遠程消息,它得給出一個methodSignatureForSelector:來返回準確的方法描述,這個方法會最終響應被轉發(fā)的消息。比如一個對象能給它的替代者對象轉發(fā)消息,它需要像下面這樣實現(xiàn)methodSignatureForSelector:

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
    NSMethodSignature* signature = [super methodSignatureForSelector:selector];
    if (!signature) {
       signature = [surrogate methodSignatureForSelector:selector];
    }
    return signature;
}

Objective-C Associated Objects

在 OS X 10.6 之后,Runtime系統(tǒng)讓Objc支持向對象動態(tài)添加變量。涉及到的函數(shù)有以下三個:

void objc_setAssociatedObject ( id object, const void *key, id value, objc_AssociationPolicy policy );
id objc_getAssociatedObject ( id object, const void *key );
void objc_removeAssociatedObjects ( id object );

這些方法以鍵值對的形式動態(tài)地向對象添加、獲取或刪除關聯(lián)值。其中關聯(lián)政策是一組枚舉常量:

enum {
   OBJC_ASSOCIATION_ASSIGN  = 0,
   OBJC_ASSOCIATION_RETAIN_NONATOMIC  = 1,
   OBJC_ASSOCIATION_COPY_NONATOMIC  = 3,
   OBJC_ASSOCIATION_RETAIN  = 01401,
   OBJC_ASSOCIATION_COPY  = 01403
};

這些常量對應著引用關聯(lián)值的政策,也就是 Objc 內(nèi)存管理的引用計數(shù)機制。

總結

我們之所以讓自己的類繼承NSObject不僅僅因為蘋果幫我們完成了復雜的內(nèi)存分配問題,更是因為這使得我們能夠用上 Runtime 系統(tǒng)帶來的便利。可能我們平時寫代碼時可能很少會考慮一句簡單的[receiver message]背后發(fā)生了什么,而只是當做方法或函數(shù)調(diào)用。深入理解 Runtime 系統(tǒng)的細節(jié)更有利于我們利用消息機制寫出功能更強大的代碼,比如 Method Swizzling 等。

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

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

  • 轉至元數(shù)據(jù)結尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 2,078評論 0 9
  • 在Objective-C中,使用 [receiver message] 語法并不會馬上執(zhí)行receiver對象的...
    Stago閱讀 255評論 0 0
  • 參考鏈接: http://www.cnblogs.com/ioshe/p/5489086.html 簡介 Runt...
    樂樂的簡書閱讀 2,249評論 0 9
  • 轉載:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麥子閱讀 839評論 0 2
  • 本文詳細整理了 Cocoa 的 Runtime 系統(tǒng)的知識,它使得 Objective-C 如虎添翼,具備了靈活的...
    lylaut閱讀 867評論 0 4

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