runtime消息轉發(fā)機制

Objective-C 擴展了 C 語言,并加入了面向對象特性和 Smalltalk 式的消息傳遞機制。而這個擴展的核心是一個用 C 和 編譯語言 寫的 Runtime 庫。它是 Objective-C 面向對象和動態(tài)機制的基石。

Objective-C 是一個動態(tài)語言,這意味著它不僅需要一個編譯器,也需要一個運行時系統(tǒng)來動態(tài)得創(chuàng)建類和對象、進行消息傳遞和轉發(fā)。理解 Objective-C 的 Runtime 機制可以幫我們更好的了解這個語言,適當的時候還能對語言進行擴展,從系統(tǒng)層面解決項目中的一些設計或技術問題。了解 Runtime ,要先了解它的核心 - 消息傳遞 (Messaging)。

消息傳遞(Messaging)
在很多語言,比如 C ,調用一個方法其實就是跳到內存中的某一點并開始執(zhí)行一段代碼。沒有任何動態(tài)的特性,因為這在編譯時就決定好了。而在 Objective-C 中,[object foo] 語法并不會立即執(zhí)行 foo 這個方法的代碼。它是在運行時給 object 發(fā)送一條叫 foo 的消息。這個消息,也許會由 object 來處理,也許會被轉發(fā)給另一個對象,或者不予理睬假裝沒收到這個消息。多條不同的消息也可以對應同一個方法實現(xiàn)。這些都是在程序運行的時候決定的。

事實上,在編譯時你寫的 Objective-C 函數調用的語法都會被翻譯成一個 C 的函數調用 - objc_msgSend() 。比如,下面兩行代碼就是等價的:

[array insertObject:foo atIndex:5];

objc_msgSend(array, @selector(insertObject:atIndex:), foo, 5);

消息傳遞的關鍵藏于 objc_object 中的 isa 指針和 objc_class 中的 class dispatch table。

objc_object, objc_class 以及 Ojbc_method
在 Objective-C 中,類、對象和方法都是一個 C 的結構體,從 objc/objc.h 頭文件中,我們可以找到他們的定義:

struct objc_object {  
    Class isa  OBJC_ISA_AVAILABILITY;
};

struct objc_class {  
    Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class super_class;
    const char *name;
    long version;
    long info;
    long instance_size;
    struct objc_ivar_list *ivars;
    **struct objc_method_list **methodLists**;
    **struct objc_cache *cache**;
    struct objc_protocol_list *protocols;
#endif
};

struct objc_method_list {  
    struct objc_method_list *obsolete;
    int method_count;

#ifdef __LP64__
    int space;
#endif

    /* variable length structure */
    struct objc_method method_list[1];
};

struct objc_method {  
    SEL method_name;
    char *method_types;    /* a string representing argument/return types */
    IMP method_imp;
};

objc_method_list 本質是一個有 objc_method 元素的可變長度的數組。一個 objc_method 結構體中有函數名,也就是SEL,有表示函數類型的字符串 (見 Type Encoding) ,以及函數的實現(xiàn)IMP。

從這些定義中可以看出發(fā)送一條消息也就 objc_msgSend 做了什么事。舉 objc_msgSend(obj, foo) 這個例子來說:

首先,通過 obj 的 isa 指針找到它的 class ;
在 class 的 method list 找 foo ;
如果 class 中沒到 foo,繼續(xù)往它的 superclass 中找 ;
一旦找到 foo 這個函數,就去執(zhí)行它的實現(xiàn)IMP .
但這種實現(xiàn)有個問題,效率低。但一個 class 往往只有 20% 的函數會被經常調用,可能占總調用次數的 80% 。每個消息都需要遍歷一次 objc_method_list 并不合理。如果把經常被調用的函數緩存下來,那可以大大提高函數查詢的效率。這也就是 objc_class 中另一個重要成員 objc_cache 做的事情 - 再找到 foo 之后,把 foo 的 method_name 作為 key ,method_imp 作為 value 給存起來。當再次收到 foo 消息的時候,可以直接在 cache 里找到,避免去遍歷 objc_method_list.

動態(tài)方法解析和轉發(fā)
在上面的例子中,如果 foo 沒有找到會發(fā)生什么?通常情況下,程序會在運行時掛掉并拋出 unrecognized selector sent to … 的異常。但在異常拋出前,Objective-C 的運行時會給你三次拯救程序的機會:

Method resolution
Fast forwarding
Normal forwarding
Method Resolution
首先,Objective-C 運行時會調用 +resolveInstanceMethod: 或者 +resolveClassMethod:,讓你有機會提供一個函數實現(xiàn)。如果你添加了函數并返回 YES, 那運行時系統(tǒng)就會重新啟動一次消息發(fā)送的過程。還是以 foo 為例,你可以這么實現(xiàn):

void fooMethod(id obj, SEL _cmd)  
{
    NSLog(@"Doing foo");
}

+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if(aSEL == @selector(foo:)){
        class_addMethod([self class], aSEL, (IMP)fooMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod];
}

Core Data 有用到這個方法。NSManagedObjects 中 properties 的 getter 和 setter 就是在運行時動態(tài)添加的。

如果 resolve 方法返回 NO ,運行時就會移到下一步:消息轉發(fā)(Message Forwarding)。

PS:iOS 4.3 加入很多新的 runtime 方法,主要都是以 imp 為前綴的方法,比如 imp_implementationWithBlock() 用 block 快速創(chuàng)建一個 imp 。
上面的例子可以重寫成:

IMP fooIMP = imp_implementationWithBlock(^(id _self) {  
    NSLog(@"Doing foo");
});

class_addMethod([self class], aSEL, fooIMP, "v@:");  

Fast forwarding
如果目標對象實現(xiàn)了 -forwardingTargetForSelector: ,Runtime 這時就會調用這個方法,給你把這個消息轉發(fā)給其他對象的機會。

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

只要這個方法返回的不是 nil 和 self,整個消息發(fā)送的過程就會被重啟,當然發(fā)送的對象會變成你返回的那個對象。否則,就會繼續(xù) Normal Fowarding 。

這里叫 Fast ,只是為了區(qū)別下一步的轉發(fā)機制。因為這一步不會創(chuàng)建任何新的對象,但下一步轉發(fā)會創(chuàng)建一個 NSInvocation 對象,所以相對更快點。

Normal forwarding
這一步是 Runtime 最后一次給你挽救的機會。首先它會發(fā)送 -methodSignatureForSelector: 消息獲得函數的參數和返回值類型。如果 -methodSignatureForSelector: 返回 nil ,Runtime 則會發(fā)出 -doesNotRecognizeSelector: 消息,程序這時也就掛掉了。如果返回了一個函數簽名,Runtime 就會創(chuàng)建一個 NSInvocation 對象并發(fā)送 -forwardInvocation: 消息給目標對象。

NSInvocation 實際上就是對一個消息的描述,包括selector 以及參數等信息。所以你可以在 -forwardInvocation: 里修改傳進來的 NSInvocation 對象,然后發(fā)送 -invokeWithTarget: 消息給它,傳進去一個新的目標:

- (void)forwardInvocation:(NSInvocation *)invocation
{
    SEL sel = invocation.selector;

    if([alternateObject respondsToSelector:sel]) {
        [invocation invokeWithTarget:alternateObject];
    } 
    else {
        [self doesNotRecognizeSelector:sel];
    }
}

Cocoa 里很多地方都利用到了消息傳遞機制來對語言進行擴展,如 Proxies、NSUndoManager 跟 Responder Chain。NSProxy 就是專門用來作為代理轉發(fā)消息的;NSUndoManager 截取一個消息之后再發(fā)送;而 Responder Chain 保證一個消息轉發(fā)給合適的響應者。

總結
Objective-C 中給一個對象發(fā)送消息會經過以下幾個步驟:

在對象類的 dispatch table 中嘗試找到該消息。如果找到了,跳到相應的函數IMP去執(zhí)行實現(xiàn)代碼;
如果沒有找到,Runtime 會發(fā)送 +resolveInstanceMethod: 或者 +resolveClassMethod: 嘗試去 resolve 這個消息;
如果 resolve 方法返回 NO,Runtime 就發(fā)送 -forwardingTargetForSelector: 允許你把這個消息轉發(fā)給另一個對象;
如果沒有新的目標對象返回, Runtime 就會發(fā)送 -methodSignatureForSelector:和 -forwardInvocation: 消息。你可以發(fā)送 -invokeWithTarget: 消息來手動轉發(fā)消息或者發(fā)送 -doesNotRecognizeSelector: 拋出異常。
利用 Objective-C 的 runtime 特性,我們可以自己來對語言進行擴展,解決項目開發(fā)中的一些設計和技術問題。下一篇文章,我會介紹 Method Swizzling 技術以及如何利用 Method Swizzling 做 Logging。

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

相關閱讀更多精彩內容

  • Objective-C 擴展了 C 語言,并加入了面向對象特性和 Smalltalk 式的消息傳遞機制。而這個擴展...
    Zsz丶少閱讀 344評論 0 0
  • 轉至元數據結尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 2,074評論 0 9
  • 這篇文章完全是基于南峰子老師博客的轉載 這篇文章完全是基于南峰子老師博客的轉載 這篇文章完全是基于南峰子老師博客的...
    西木閱讀 30,893評論 33 466
  • 1> 什么是runtimeruntime是一套比較底層的純C語言API, 屬于1個C語言庫, 包含了很多底層的C語...
    maniacRadish閱讀 353評論 0 1
  • 嬌紅圓麗, 未嘗先奪眾人愛。 肉甜汁潤, 珍品它頭牌。 ...
    老槐樹閱讀 197評論 2 4

友情鏈接更多精彩內容