runtime之objc_msgSend和消息轉(zhuǎn)發(fā)

前言

OC是一門動態(tài)語言妻柒,就預(yù)示這光靠編譯過程來完全讀懂工作的方式是不夠的百姓,很多執(zhí)行語句都需要在運行時才能知道其意思楣颠,也就是runtime笛洛。

理解OC中的消息傳遞

在Object-C中夏志,函數(shù)的調(diào)用過程經(jīng)常性的使用到方法,用來傳遞消息。而消息包括名稱或選擇子(selector)沟蔑,以及參數(shù)和返回值湿诊。
oc是一門動態(tài)語言,也就說明其消息可以是動態(tài)綁定的過程瘦材,在編譯時厅须,無法決定運行時該調(diào)用的函數(shù)。
例如該語句我們沒有辦法去知道返回值的類型:

id value = [Person sendMessage:@"黃"];

整個合起來就是一個消息食棕,在其中朗和,Person為接收者(receiver),sendMessage為選擇子簿晓,后續(xù)為參數(shù)眶拉。當(dāng)編譯器收到該條消息是,通過C語言函數(shù)憔儿,也是消息傳遞機制中的核心函數(shù)objc_msgSend進行消息傳遞忆植。

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

該函數(shù)能接受多個參數(shù),其中第一個參數(shù)代表接收者谒臼,第二個參數(shù)是選擇子朝刊,后跟可以跟多個發(fā)送消息參數(shù)。

objc_msgSend 消息傳遞機制

objc_msgSend會先判斷接收者對象是否存在蜈缤,如果存在情況根據(jù)接收者所屬的類中搜尋”方法列表“拾氓,如果找不到,則會向上找繼承類的”方法列表“底哥,找打找到合適就跳轉(zhuǎn)咙鞍,如果找不到則執(zhí)行消息轉(zhuǎn)發(fā)(后期會更新消息轉(zhuǎn)發(fā)機制)。

該過程雖然繁瑣叠艳,但是objc_msgSend會為將每將匹配的結(jié)果緩存到”快速映射表“中奶陈,每個類都存在這樣的緩存易阳,后續(xù)若是需要再發(fā)送同樣的消息附较,可以直接查找映射表,節(jié)省執(zhí)行時間潦俺。

后續(xù)消息處理工作拒课,會交給一些函數(shù)來處理:
objc_msgSend_stret如果待發(fā)送的消息要返回結(jié)構(gòu)體,可交由此函數(shù)處理事示。
objc_msgSend_fpret如果待發(fā)送的消息要返回浮點數(shù)早像,可交由此函數(shù)處理。
objc_msgSendSuper如果給父類發(fā)送消息肖爵,可交由此函數(shù)處理卢鹦。

前面提到消息一旦找到就會跳轉(zhuǎn)過去,之所以可以跳轉(zhuǎn)劝堪,是因為OC對象的每個方法視為簡單的C函數(shù)冀自。

void Class_selector(id self, SEL cmd, ...)

每個類都有一張表揉稚,其中的指針都會指向這種函數(shù),oc則以方法名作為key熬粗,來查表并執(zhí)行跳轉(zhuǎn)搀玖。

為了優(yōu)化跳轉(zhuǎn)方法變得簡單一些,采用”尾調(diào)用優(yōu)化“方式:如果某函數(shù)的最后一個操作是跳轉(zhuǎn)到另一個函數(shù)驻呐,編譯器會生成跳轉(zhuǎn)所需的指令碼灌诅,而不是向棧中新增新的幀,這樣減少了含末,每次調(diào)用objc_msgSend需要新增棧幀的問題猜拾,還有減少”棧溢出“的發(fā)生。但是僅在調(diào)用其他函數(shù)時答渔,而不會把返回值另做他用的情況下关带。

根據(jù)上面的描述總結(jié)消息發(fā)送的步驟:

  1. 檢查接收者對象是否為nil,是則直接結(jié)束轉(zhuǎn)發(fā)沼撕,否則進入下一步宋雏;
  2. 查看類緩存的”快速映射表“中是否緩存該方法,該映射方法务豺,是直接發(fā)送消息磨总,否則進入下一步;
  3. 查看類的”方法列表“中是否存在笼沥,是緩存到映射表蚪燕,并發(fā)送消息,否則進入下一步奔浅;
  4. 查看父類的”快速映射表“是否緩存改方法馆纳,有就發(fā)送,否則進入下一步汹桦;
  5. 查看父類的"方法列表"中是否存在鲁驶,是緩存到映射表,并發(fā)送消息舞骆,否則回到第4步繼續(xù)查找更上層的父類钥弯;
  6. 查找到?jīng)]有父類之后,就說明并沒有該方法督禽,但是還不一定結(jié)束脆霎,后面會去進入消息轉(zhuǎn)發(fā)機制的步驟。
動態(tài)方法解析

上述說到狈惫,當(dāng)發(fā)送消息時睛蛛,會出現(xiàn)無法解讀消息的情況。即當(dāng)消息發(fā)送到最后,依然找不到接收消息的方式時忆肾,oc對該消息可以進行更多處理菠红,也就是動態(tài)方法解析消息轉(zhuǎn)發(fā)

oc是動態(tài)語言难菌,在消息找不到目標(biāo)時试溯,編譯器無法解析錯誤情況并不會報錯,因為在運行時可以動態(tài)的給類添加方法郊酒,所以當(dāng)無法解析消息是遇绞,就會啟動消息轉(zhuǎn)發(fā)機制,可由程序員手動動態(tài)處理后續(xù)的消息問題燎窘。

如果程序員沒有去處理這些問題摹闽,則會直接crach掉:

- (void)viewDidLoad {
    [super viewDidLoad];
    // 去調(diào)用一個沒有實現(xiàn)的方法后
    [self test];
}
// 刪掉實現(xiàn)方法
//- (void)test { }

// 閃退并打印以下
*** -[ViewController test]: unrecognized selector sent to instance 0x100e01440
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', 
reason: '-[ViewController test]: unrecognized selector sent to instance 0x100e01440'

我們只調(diào)用test方法,但是沒有調(diào)用實現(xiàn)方法褐健,因此付鹿,如果沒有做異常處理的話,會直接閃退蚜迅。

消息轉(zhuǎn)發(fā)兩個階段:

第一階段是動態(tài)方法解析舵匾,先征詢接收者所屬的類,看是否能動態(tài)添加方法谁不,以處理當(dāng)>前”未知選擇子(調(diào)用的方法)“坐梯。
第二階段涉及完整的消息轉(zhuǎn)發(fā)機制,若是第一階段可以順利完成刹帕,就不會啟動消息轉(zhuǎn)發(fā)吵血。

顯然如果能越早對消息進行處理,則減少運行時所耗費的時長偷溺。

動態(tài)方法解析有兩個方法蹋辅,包括實例方法解析和類方法解析

+ (BOOL)resolveClassMethod:(SEL)sel
+ (BOOL)resolveInstanceMethod:(SEL)sel

在該例子中,如所屬類添加了resolveInstanceMethod方法去處理挫掏,當(dāng)調(diào)用的方法沒有找到實現(xiàn)目標(biāo)時侦另,就會調(diào)用該實例方法去處理。

void dynamicMethodIMP(id self, SEL _cmd) { }
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 去調(diào)用一個沒有實現(xiàn)的方法后
    [self test];
}
// 刪掉實現(xiàn)方法
//- (void)test { }

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(test)) {
        class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
@end

如上圖所示砍濒,不會異常閃退淋肾,因為當(dāng)調(diào)用test方法時硫麻,找不到會直接調(diào)用resolveInstanceMethod爸邢,而在resolveInstanceMethod中對test,通過class_addMethod動態(tài)添加拿愧。

備援接收者

如果resolveInstanceMethod或者resolveClassMethod返回YES就直接結(jié)束杠河,如果返回NO,說明動態(tài)解析沒有創(chuàng)建動態(tài)方法去處理,當(dāng)前接收者還有第二次機會能處理未知選擇子券敌,在運行時唾戚,到這一步,就會問這條消息能不能轉(zhuǎn)發(fā)給其他接收者處理待诅。

- (id)forwardingTargetForSelector:(SEL)aSelector {
    Person *person = [[Person alloc]init];

    if (aSelector == @selector(test)) {
        return person;
    } else {
       return [super forwardingTargetForSelector:aSelector];
    }
}

當(dāng)調(diào)用到這一步叹坦,如果返回對象,則是在告訴編譯器卑雁,把這條消息傳遞給person對象來處理募书。到這里就不會出現(xiàn)異常,如果直接返回nil或者沒有該對象测蹲,說明依然沒辦法處理結(jié)束并拋出異常莹捡。

注意:我們無法經(jīng)由這一步操作消息轉(zhuǎn)發(fā),如果需要修改消息的內(nèi)容扣甲,就要啟用完整的消息轉(zhuǎn)發(fā)機制篮赢。

完整的消息轉(zhuǎn)發(fā)

到這一步,說明只能啟用消息轉(zhuǎn)發(fā)琉挖,通過將有關(guān)消息的全部內(nèi)容启泣,創(chuàng)建NSInvocation對象,并封裝內(nèi)容包括選擇子示辈、目標(biāo)及參數(shù)种远。再觸發(fā)NSInvocation對象時,通過’消息派發(fā)系統(tǒng)‘將消息指派給目標(biāo)對象顽耳。

- (void)forwardInvocation:(NSInvocation *)anInvocation

但是使用消息轉(zhuǎn)發(fā)時必須為另一個類實現(xiàn)的消息創(chuàng)建一個有效的方法簽名:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

實現(xiàn)的方式如下所致坠敷,其與第二步消息轉(zhuǎn)發(fā)等效,但是卻可以在消息觸發(fā)前射富,改變消息的內(nèi)容膝迎。比如:追加一個參數(shù)或者改變選擇子等。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if(aSelector == @selector(test))
    {
      return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return nil;
}


- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = [anInvocation selector];
    // 新建需要轉(zhuǎn)發(fā)消息的對象
    Cat *cat = [[Cat alloc] init];
    if ([cat respondsToSelector:selector]) {
        // 喚醒這個方法
        [anInvocation invokeWithTarget:cat];
    }
}

以上就是消息轉(zhuǎn)發(fā)的幾個步驟胰耗,每一步均有機會處理消息限次。步驟越往后,處理消息的代價就越大柴灯,如果可以在第一步就把消息處理完卖漫,運行時也可以將消息緩存起來。如果放到最后再做處理的話赠群,不僅復(fù)雜還需要創(chuàng)建和處理完整的NSInvocation對象羊始。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市查描,隨后出現(xiàn)的幾起案子突委,更是在濱河造成了極大的恐慌柏卤,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,599評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件匀油,死亡現(xiàn)場離奇詭異缘缚,居然都是意外死亡,警方通過查閱死者的電腦和手機敌蚜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,629評論 3 385
  • 文/潘曉璐 我一進店門桥滨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人弛车,你說我怎么就攤上這事该园。” “怎么了帅韧?”我有些...
    開封第一講書人閱讀 158,084評論 0 348
  • 文/不壞的土叔 我叫張陵里初,是天一觀的道長。 經(jīng)常有香客問我忽舟,道長双妨,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,708評論 1 284
  • 正文 為了忘掉前任叮阅,我火速辦了婚禮刁品,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘浩姥。我一直安慰自己挑随,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,813評論 6 386
  • 文/花漫 我一把揭開白布勒叠。 她就那樣靜靜地躺著兜挨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪眯分。 梳的紋絲不亂的頭發(fā)上拌汇,一...
    開封第一講書人閱讀 50,021評論 1 291
  • 那天,我揣著相機與錄音弊决,去河邊找鬼噪舀。 笑死,一個胖子當(dāng)著我的面吹牛飘诗,可吹牛的內(nèi)容都是我干的与倡。 我是一名探鬼主播,決...
    沈念sama閱讀 39,120評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼昆稿,長吁一口氣:“原來是場噩夢啊……” “哼纺座!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起貌嫡,我...
    開封第一講書人閱讀 37,866評論 0 268
  • 序言:老撾萬榮一對情侶失蹤比驻,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后岛抄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體别惦,經(jīng)...
    沈念sama閱讀 44,308評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,633評論 2 327
  • 正文 我和宋清朗相戀三年夫椭,在試婚紗的時候發(fā)現(xiàn)自己被綠了掸掸。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,768評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡蹭秋,死狀恐怖扰付,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情仁讨,我是刑警寧澤羽莺,帶...
    沈念sama閱讀 34,461評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站洞豁,受9級特大地震影響盐固,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜丈挟,卻給世界環(huán)境...
    茶點故事閱讀 40,094評論 3 317
  • 文/蒙蒙 一刁卜、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧曙咽,春花似錦蛔趴、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,850評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至洒嗤,卻和暖如春咧叭,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背烁竭。 一陣腳步聲響...
    開封第一講書人閱讀 32,082評論 1 267
  • 我被黑心中介騙來泰國打工菲茬, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人派撕。 一個月前我還...
    沈念sama閱讀 46,571評論 2 362
  • 正文 我出身青樓婉弹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親终吼。 傳聞我的和親對象是個殘疾皇子镀赌,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,666評論 2 350