理解oc消息傳遞機(jī)制

object-c語言的動態(tài)性

Objective-C 是一門極其動態(tài)的語言,許多東西都可以推遲到運行時決定寂玲、修改殷绍。那么到底何為動態(tài)、何為靜態(tài)桐经?我們通過一個簡單的例子對比下

/***********  例1 靜態(tài)綁定   ***********/
#import <stdio.h>
void printHello() {
    printf("Hello, world!\n");
}
void printGoodbye() {
    printf("Goodbye, world!\n");
}
void saySomething(int type) 
{
    if (type == 0) {
        printHello();
    } else {
        printGoodbye();
    }
    return 0;
}
/***********  例2 動態(tài)綁定   ***********/
#import <stdio.h>
void printHello() {
    printf("Hello, world!\n");
}
void printGoodbye() {
    printf("Goodbye, world!\n");
}
void saySomething(int type) 
{
    void (*func)();
    if (type == 0) {
        func = printHello;
    } else {
        func = printGoodbye;
    }
    func();
    return 0;
}

例1的代碼在編譯期毁兆,編譯器就已經(jīng)知道了有 void printHello()、void printGoodbye() 倆函數(shù)阴挣,并且在 saySomething() 函數(shù)中气堕,調(diào)用的函數(shù)明確,可以直接將函數(shù)名硬編碼成地址畔咧,生成調(diào)用指令茎芭,這就是 靜態(tài)綁定(static binding)。那么例2呢誓沸?例2的調(diào)用的是 func() 函數(shù)梅桩,而這函數(shù)實際調(diào)用的地址只能到程序運行時才能確定,這就是所謂的 動態(tài)綁定(dynamic binding)拜隧。動態(tài)綁定將函數(shù)調(diào)用從編譯期推遲到了運行時宿百。

在 Objective-C 中趁仙,向?qū)ο髠鬟f消息,就會使用這種動態(tài)綁定機(jī)制來決定需要調(diào)用的方法垦页,這種動態(tài)特性使得 Objective-C 成為一門真正動態(tài)的語言雀费。

objec_msgSend 函數(shù)

Objective-C 的方法調(diào)用通常都是下面這種形式

id returnValue = [someObject messageName:parameter];

這種方法調(diào)用其實就是消息傳遞,編譯器看到這條消息會轉(zhuǎn)換成一條標(biāo)準(zhǔn)的 C 語言函數(shù)調(diào)用

id returnValue = objc_msgSend(someObject,
                              @selector(messageName:),
                              parameter);

用消息傳遞的話來解釋就是:向 someObject 對象發(fā)送了一個名叫 messageName 的消息痊焊,這個消息攜帶了一個叫 parameter 的參數(shù)坐儿。這里用到了一個 objc_msgSend 函數(shù),其函數(shù)原型如下

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

這是一個可變參數(shù)的函數(shù)宋光,第一個參數(shù)代表消息接收者貌矿,第二個代表 SEL 類型,后面的參數(shù)就是消息傳遞中使用的參數(shù)罪佳。

那么什么是 SEL 呢逛漫?SEL 就是代碼在編譯時,編譯器根據(jù)方法簽名來生成的一個唯一 ID赘艳。此 ID 可以用以區(qū)分不同的方法酌毡,只要 ID 一致,即看成同一個方法蕾管,ID 不同枷踏,即為不同的方法。

當(dāng)進(jìn)行消息傳遞掰曾,對象在響應(yīng)消息時旭蠕,是通過 SEL 在 methodlist 中查找函數(shù)指針 IMP,找到后直接通過指針調(diào)用函數(shù)旷坦,這其實就是前文介紹的 動態(tài)綁定掏熬。若是找到對應(yīng)函數(shù)就跳轉(zhuǎn)到實現(xiàn)代碼,若找不到秒梅,就沿著繼承鏈往上查找旗芬,直到找到相應(yīng)的實現(xiàn)代碼為止。若最終還是沒找到實現(xiàn)代碼捆蜀,說明當(dāng)前對象無法響應(yīng)此消息疮丛,接下來就會執(zhí)行 消息轉(zhuǎn)發(fā) 操作,以試圖找到一個能響應(yīng)此消息的對象辆它。

// 獲取 SEL 
SEL sel = @selector(methodName);
// 獲取 IMP
IMP imp = methodForSelector(sel);

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

消息轉(zhuǎn)發(fā)并不神奇誊薄,我們其實早已接觸過,只是不知道而已

-[__NSCFNumber lowercaseString]:unrecognized selector sent to instance 0x87
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason:'-[NSCFNumber lowercaseString]:unrecognized selector sent to instance 0x87'

這段異常代碼就是由 NSObject 的 doesNotRecognizeSelector: 方法所拋出的娩井,異常表明:消息的接收者類型為 __NSCFNumber暇屋,無法響應(yīng) lowercaseString 消息,從而轉(zhuǎn)發(fā)給 NSObject 處理洞辣。

消息轉(zhuǎn)發(fā)分為三大階段

  • 第一階段先征詢消息接收者所屬的類咐刨,看其是否能動態(tài)添加方法昙衅,以處理當(dāng)前這個無法響應(yīng)的 selector,這叫做 動態(tài)方法解析(dynamic method resolution)定鸟。如果運行期系統(tǒng)(runtime system) 第一階段執(zhí)行結(jié)束而涉,接收者就無法再以動態(tài)新增方法的手段來響應(yīng)消息,進(jìn)入第二階段联予。
  • 第二階段看看有沒有其他對象(備援接收者啼县,replacement receiver)能處理此消息。如果有沸久,運行期系統(tǒng)會把消息轉(zhuǎn)發(fā)給那個對象季眷,轉(zhuǎn)發(fā)過程結(jié)束;如果沒有卷胯,則啟動完整的消息轉(zhuǎn)發(fā)機(jī)制子刮。
  • 第三階段 完整的消息轉(zhuǎn)發(fā)機(jī)制。運行期系統(tǒng)會把與消息有關(guān)的全部細(xì)節(jié)都封裝到 NSInvocation 對象中窑睁,再給接收者最后一次機(jī)會挺峡,令其設(shè)法解決當(dāng)前還未處理的消息。

動態(tài)方法解析

對象在收到無法響應(yīng)的消息后担钮,會調(diào)用其所屬類的下列方法

/**
 *  如果尚未實現(xiàn)的方法是實例方法橱赠,則調(diào)用此函數(shù)
 *
 *  @param selector 未處理的方法
 *
 *  @return 返回布爾值,表示是否能新增實例方法用以處理selector
 */
+ (BOOL)resolveInstanceMethod:(SEL)selector;
/**
 *  如果尚未實現(xiàn)的方法是類方法箫津,則調(diào)用此函數(shù)
 *
 *  @param selector 未處理的方法
 *
 *  @return 返回布爾值狭姨,表示是否能新增類方法用以處理selector
 */
+ (BOOL)resolveClassMethod:(SEL)selector;

方法返回布爾類型,表示是否能新增一個方法來處理 selector鲤嫡,此方案通常用來實現(xiàn) @dynamic 屬性送挑。

// 動態(tài)方法解析,先進(jìn)行這一步
+ (BOOL)resolveInstanceMethod:(SEL)name
// 實例方法
{
    
    NSLog(@" >> Instance resolving %@", NSStringFromSelector(name));
    // MissMethod為調(diào)用的方法名
    if (name == @selector(dynamicParserMethod)) {
        
        class_addMethod([self class], name, (IMP)dynamicMethodIMP, "v@:");
        
        return YES;
        
    }
    
    return [super resolveInstanceMethod:name];
    
}
// 處理該類無法處理消息的方法
void dynamicMethodIMP(id self, SEL _cmd) {
    
    NSLog(@" >> dynamicMethodIMP");
    
}

備援接收者

如果無法 動態(tài)解析方法暖眼,運行期系統(tǒng)就會詢問是否能將消息轉(zhuǎn)給其他接收者來處理,對應(yīng)的方法為

// 備援接收者的處理
- (id)forwardingTargetForSelector:(SEL)aSelector {
    
    NSLog(@" >> forwardingTargetForSelector %@", NSStringFromSelector(aSelector));
    
    if (aSelector == @selector(testReceiveObject)) {
        // 返回接受該方法的處理類
        ReceiveObject *receiveObject = [[ReceiveObject alloc] init];
        return receiveObject;
        
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

在對象內(nèi)部纺裁,可能還有其他對象诫肠,該對象可通過此方法將能夠處理 selector 的相關(guān)內(nèi)部對象返回,在外界看來欺缘,就好像是該對象自己處理的似得栋豫。

完整的消息轉(zhuǎn)發(fā)機(jī)制

如果前面兩步都無法處理消息,就會啟動完整的消息轉(zhuǎn)發(fā)機(jī)制谚殊。首先創(chuàng)建 NSInvocation 對象丧鸯,把尚未處理的那條消息有關(guān)的全部細(xì)節(jié)裝在里面,在觸發(fā) NSInvocation 對象時嫩絮,消息派發(fā)系統(tǒng)(message-dispatch system)將會把消息指派給目標(biāo)對象丛肢。對應(yīng)的方法為

/**
 *  消息派發(fā)系統(tǒng)通過此方法围肥,將消息派發(fā)給目標(biāo)對象,實現(xiàn)消息轉(zhuǎn)發(fā)
 *
 *  @param anInvocation 之前創(chuàng)建的NSInvocation實例對象,用于裝載有關(guān)消息的所有內(nèi)容
 */
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    
    NSLog(@"forwardInvocation = %@", NSStringFromSelector(anInvocation.selector));
    
    if (anInvocation.selector == @selector(forwardInvocationMsg)) {
        
        ReceiveObject *receiveObject = [[ReceiveObject alloc] init];
        if ([receiveObject respondsToSelector:[anInvocation selector]]) {
            [anInvocation invokeWithTarget:receiveObject];
        }else {
            [super forwardInvocation:anInvocation];
        }
    }
    
}

// 調(diào)用forwardInvocation之前要先進(jìn)行對方法進(jìn)行注冊

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
    NSMethodSignature* signature = [super methodSignatureForSelector:selector];
    if (!signature) {
        // 注冊
         ReceiveObject *receiveObject = [[ReceiveObject alloc] init];
        signature = [receiveObject methodSignatureForSelector:selector];
    }
    return signature;
}

這個方法可以實現(xiàn)的很簡單蜂怎,通過改變調(diào)用的目標(biāo)對象穆刻,使得消息在新目標(biāo)對象上得以調(diào)用即可。然而這樣實現(xiàn)的效果與 備援接收者 差不多杠步,所以很少人會這么做氢伟。更加有用的實現(xiàn)方式為:在觸發(fā)消息前,先以某種方式改變消息內(nèi)容幽歼,比如追加另一個參數(shù)朵锣、修改 selector 等等。

總結(jié)

Paste_Image.png

demo地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末甸私,一起剝皮案震驚了整個濱河市猪勇,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌颠蕴,老刑警劉巖泣刹,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異犀被,居然都是意外死亡椅您,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門寡键,熙熙樓的掌柜王于貴愁眉苦臉地迎上來掀泳,“玉大人,你說我怎么就攤上這事西轩≡倍妫” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵藕畔,是天一觀的道長马僻。 經(jīng)常有香客問我,道長注服,這世上最難降的妖魔是什么韭邓? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮溶弟,結(jié)果婚禮上女淑,老公的妹妹穿的比我還像新娘。我一直安慰自己辜御,他們只是感情好鸭你,可當(dāng)我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般袱巨。 火紅的嫁衣襯著肌膚如雪阁谆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天瓣窄,我揣著相機(jī)與錄音笛厦,去河邊找鬼。 笑死俺夕,一個胖子當(dāng)著我的面吹牛裳凸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播劝贸,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼姨谷,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了映九?” 一聲冷哼從身側(cè)響起梦湘,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎件甥,沒想到半個月后捌议,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡引有,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年瓣颅,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片譬正。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡宫补,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出曾我,到底是詐尸還是另有隱情粉怕,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布抒巢,位于F島的核電站贫贝,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏虐秦。R本人自食惡果不足惜平酿,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望悦陋。 院中可真熱鬧,春花似錦筑辨、人聲如沸俺驶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽暮现。三九已至还绘,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間栖袋,已是汗流浹背拍顷。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留塘幅,地道東北人昔案。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像电媳,于是被迫代替她去往敵國和親踏揣。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,976評論 2 355

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