runtime消息轉(zhuǎn)發(fā)機制

Objective-C 擴展了 C 語言,并加入了面向?qū)ο筇匦院?Smalltalk 式的消息傳遞機制烁巫。而這個擴展的核心是一個用 C 和 編譯語言 寫的 Runtime 庫署隘。它是 Objective-C 面向?qū)ο蠛蛣討B(tài)機制的基石。

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

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

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

[array insertObject:foo atIndex:5];

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

消息傳遞的關(guān)鍵藏于 objc_object 中的 isa 指針和 objc_class 中的 class dispatch table鲸湃。

objc_object, objc_class 以及 Ojbc_method
在 Objective-C 中,類子寓、對象和方法都是一個 C 的結(jié)構(gòu)體唤锉,從 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 本質(zhì)是一個有 objc_method 元素的可變長度的數(shù)組别瞭。一個 objc_method 結(jié)構(gòu)體中有函數(shù)名窿祥,也就是SEL,有表示函數(shù)類型的字符串 (見 Type Encoding) 蝙寨,以及函數(shù)的實現(xiàn)IMP晒衩。

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

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

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

Method resolution
Fast forwarding
Normal forwarding
Method Resolution
首先,Objective-C 運行時會調(diào)用 +resolveInstanceMethod: 或者 +resolveClassMethod:枣购,讓你有機會提供一個函數(shù)實現(xiàn)嬉探。如果你添加了函數(shù)并返回 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 胎围,運行時就會移到下一步:消息轉(zhuǎn)發(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 這時就會調(diào)用這個方法福荸,給你把這個消息轉(zhuǎn)發(fā)給其他對象的機會。

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

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

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

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

NSInvocation 實際上就是對一個消息的描述,包括selector 以及參數(shù)等信息年堆。所以你可以在 -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 就是專門用來作為代理轉(zhuǎn)發(fā)消息的痒蓬;NSUndoManager 截取一個消息之后再發(fā)送童擎;而 Responder Chain 保證一個消息轉(zhuǎn)發(fā)給合適的響應(yīng)者滴劲。

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市几莽,隨后出現(xiàn)的幾起案子迅办,更是在濱河造成了極大的恐慌,老刑警劉巖章蚣,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件站欺,死亡現(xiàn)場離奇詭異,居然都是意外死亡纤垂,警方通過查閱死者的電腦和手機矾策,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來峭沦,“玉大人贾虽,你說我怎么就攤上這事『鹩悖” “怎么了蓬豁?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長菇肃。 經(jīng)常有香客問我地粪,道長,這世上最難降的妖魔是什么琐谤? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任蟆技,我火速辦了婚禮罢猪,結(jié)果婚禮上峦睡,老公的妹妹穿的比我還像新娘能犯。我一直安慰自己缀拭,他們只是感情好,可當我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布眶蕉。 她就那樣靜靜地躺著砰粹,像睡著了一般。 火紅的嫁衣襯著肌膚如雪妻坝。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天惊窖,我揣著相機與錄音刽宪,去河邊找鬼。 笑死界酒,一個胖子當著我的面吹牛圣拄,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播毁欣,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼庇谆,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了凭疮?” 一聲冷哼從身側(cè)響起饭耳,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎执解,沒想到半個月后寞肖,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡衰腌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年新蟆,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片右蕊。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡琼稻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出饶囚,到底是詐尸還是另有隱情帕翻,我是刑警寧澤,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布萝风,位于F島的核電站熊咽,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏闹丐。R本人自食惡果不足惜横殴,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧衫仑,春花似錦梨与、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至瞄崇,卻和暖如春呻粹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背苏研。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工等浊, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人摹蘑。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓筹燕,卻偏偏與公主長得像,于是被迫代替她去往敵國和親衅鹿。 傳聞我的和親對象是個殘疾皇子撒踪,可洞房花燭夜當晚...
    茶點故事閱讀 43,490評論 2 348

推薦閱讀更多精彩內(nèi)容

  • Objective-C 擴展了 C 語言,并加入了面向?qū)ο筇匦院?Smalltalk 式的消息傳遞機制大渤。而這個擴展...
    Zsz丶少閱讀 299評論 0 0
  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉制妄,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,686評論 0 9
  • 這篇文章完全是基于南峰子老師博客的轉(zhuǎn)載 這篇文章完全是基于南峰子老師博客的轉(zhuǎn)載 這篇文章完全是基于南峰子老師博客的...
    西木閱讀 30,548評論 33 466
  • 1> 什么是runtimeruntime是一套比較底層的純C語言API, 屬于1個C語言庫, 包含了很多底層的C語...
    maniacRadish閱讀 300評論 0 1
  • 嬌紅圓麗, 未嘗先奪眾人愛泵三。 肉甜汁潤忍捡, 珍品它頭牌。 ...
    老槐樹閱讀 175評論 2 4