當(dāng)我們像一個對象發(fā)送消息[Receiver message]衔彻,Receiver沒有實(shí)現(xiàn)該消息,即[Receiver respondsToSelector:SEL]返回為NO情況下戚揭,其實(shí)系統(tǒng)不會立刻出現(xiàn)cash,這時Runtime system會對message進(jìn)行轉(zhuǎn)發(fā)缔俄。轉(zhuǎn)發(fā)之后烙懦,如果該消息依然沒有被執(zhí)行就會出現(xiàn)Cash!Runtime System為我們提供了三種解決這種給對象發(fā)送沒有實(shí)現(xiàn)消息方案昆烁。
消息轉(zhuǎn)發(fā)機(jī)制基本上分為三個步驟:
1. 動態(tài)方法解析
2. 備用接收者
3. 完整轉(zhuǎn)發(fā)
我們可以通過控制這三個步驟其中一環(huán)來解決這一個問題
特別注意:如果是正常類的消息吊骤,是不會走到這三個步驟的。所以走到這三個不步驟的前提條件已經(jīng)確定該消息為未知消息
這篇博客的前置知識點(diǎn)是 OC 的消息傳遞機(jī)制静尼,如果你對此還不了解白粉,請先學(xué)習(xí)之,再來看這篇鼠渺。這篇博客我嘗試用口語的方式像講述 PPT 一樣給大家講述這個知識點(diǎn)鸭巴。
我們來思考一個問題,如果對象在收到無法解讀的消息時拦盹,會發(fā)生什么鹃祖?例如,我們實(shí)現(xiàn)一個 viewcontroller掌敬,其中并沒有一個成員方法名為『setText:』,當(dāng)編寫這條語句時
[selfsetText:@"你好"];
示例
由于 OC 是一門動態(tài)語言惯豆,在編譯期只是顯示一條 warning池磁,而不是阻止運(yùn)行的 error奔害。如果忽略 warning 運(yùn)行楷兽,程序會 crash,在控制臺會顯示類似
unrecognized selector sent to instance0x7f931a4180d0
的報(bào)錯信息华临。
unrecognized selector
消息被發(fā)送給了不能處理它的對象芯杀。我們學(xué)習(xí) iOS 的消息轉(zhuǎn)發(fā)機(jī)制可不是為了故意造這樣的 crash 玩,說上面的這個例子雅潭,是為了說明如果我們不通過消息轉(zhuǎn)發(fā)機(jī)制做任何事情的話揭厚,系統(tǒng)最終會以 crash 結(jié)束。等等扶供,剛才我們說到 OC 是一門動態(tài)語言筛圆,那么是否可以在運(yùn)行期做一些事來讓 crash 不會發(fā)生呢?
消息轉(zhuǎn)發(fā)機(jī)制就是來干這件事的椿浓,在運(yùn)行期通過3個『接盤俠』方法太援,給對象和消息更多的機(jī)會來完成成功的調(diào)用,而不是直接 crash扳碍。
一號接盤俠
第一個接盤俠代表動態(tài)方法解析階段提岔,對應(yīng)的具體方法是+(BOOL)resolveInstanceMethod:(SEL)sel 和+(BOOL)resolveClassMethod:(SEL)sel,當(dāng)方法是實(shí)例方法時調(diào)用前者笋敞,當(dāng)方法為類方法時碱蒙,調(diào)用后者。這個方法設(shè)計(jì)的目的是為了給類利用 class_addMethod 添加方法的機(jī)會夯巷。
看下面這個示例赛惩,MyTestObject類重寫了第一個接盤俠方法,可以看到這個方法傳入一個 selector趁餐,返回 BOOL 類型坊秸。被傳入的 selector 就是未被處理的方法,在一號接盤俠方法中澎怒,判斷若方法名為 XXX 則給這個類添加同名的方法褒搔,把方法的實(shí)現(xiàn)指向跟 XXX 名字不一致的 AAA,并返回 YES喷面。若 selector 名字不是 XXX星瘾,就返回父類。
resolveInstanceMethod
通過這個示例惧辈,可以看出琳状,我們可以通過一號接盤俠方法讓 方法名和方法實(shí)現(xiàn)在運(yùn)行期任意搭配。
再說一下這個返回值盒齿,其實(shí)可以試驗(yàn)一下念逞,無論返回 YES 還是 NO困食,系統(tǒng)都會嘗試用 SEL 來尋找 IMP,如果找到函數(shù)實(shí)現(xiàn)翎承,則執(zhí)行硕盹,所以無論返回 YES\NO都會進(jìn)入二號接盤俠方法。
二號接盤俠
第二個階段是備援接收者階段叨咖,對象的具體方法是-(id)forwardingTargetForSelector:(SEL)aSelector 瘩例,此時,運(yùn)行時詢問能否把消息轉(zhuǎn)給其他接收者處理甸各,也就是此時系統(tǒng)給了個將這個 SEL 轉(zhuǎn)給其他對象的機(jī)會垛贤。我們繼續(xù)來研究下參數(shù)和返回值,參數(shù)和一號接盤俠一樣趣倾,都是 selector聘惦,返回值是 id 類型,當(dāng)返回 非self\非nil 時儒恋,消息被轉(zhuǎn)給新對象執(zhí)行善绎。
forwardingTargetForSelector
三號接盤俠
第三個階段是完整消息轉(zhuǎn)發(fā)階段,對應(yīng)方法-(void)forwardInvocation:(NSInvocation *)anInvocation碧浊,這是消息轉(zhuǎn)發(fā)流程的最后一個環(huán)節(jié)涂邀。參數(shù) anInvocation 中包含未處理消息的各種信息(selector\target\參數(shù)...)。在這個方法中箱锐,可以把 anInvocation 轉(zhuǎn)發(fā)給多個對象比勉,與二號接盤俠不同,二號只能轉(zhuǎn)給一個對象驹止。
forwardInvocation
如果上述3個方法都沒有來處理這個消息浩聋,就會進(jìn)入 NSObject 的-(void)doesNotRecognizeSelector:(SEL)aSelector方法中,拋出異常臊恋。等等衣洁,為什么我們不能通過給 NSObject 創(chuàng)建一個 category,重寫這個方法抖仅,在這里處理消息未被處理的情況呀坊夫?在蘋果的官方文檔中,明確提到撤卢,“一定不能讓這個函數(shù)就這么結(jié)束掉环凿,必須拋出異常”放吩。除了聽官方文檔的話智听,其實(shí)在分類中通過重寫該方法處理各種消息未被處理的情況,會讓這個分類的方法特別長,不利于維護(hù)到推。而且還有個原因考赛,明明方法名叫『無法識別 selector』,其中卻是一大堆處理該情況的代碼莉测,也很奇怪颜骤。
doesNotRecognizeSelector
總結(jié)
總結(jié)一下整個消息轉(zhuǎn)發(fā)的流程:
消息轉(zhuǎn)發(fā)的流程
可以通過重寫3個接盤俠方法,在其中打斷點(diǎn)來驗(yàn)證執(zhí)行順序悔雹。
斷點(diǎn)驗(yàn)證順序
總結(jié):
在一個函數(shù)找不到時复哆,OC提供了三種方式去補(bǔ)救:
1欣喧、調(diào)用resolveInstanceMethod給個機(jī)會讓類添加這個實(shí)現(xiàn)這個函數(shù)
2腌零、調(diào)用forwardingTargetForSelector讓別的對象去執(zhí)行這個函數(shù)
3、調(diào)用forwardInvocation(函數(shù)執(zhí)行器)靈活的將目標(biāo)函數(shù)以其他形式執(zhí)行唆阿。
如果都不中益涧,調(diào)用doesNotRecognizeSelector拋出異常。
疑問
Q1:那我們只用最后一個接盤俠方法多好啊驯鳖,為什么還需要前2個呢闲询?
其實(shí)還與這3個方法的用途不同有關(guān):
運(yùn)行期添加方法,用1浅辙;
轉(zhuǎn)發(fā)給另1個對象扭弧、改變方法時,用2记舆;
需要轉(zhuǎn)發(fā)給多個對象時鸽捻,用3;
而且泽腮,步驟越往后御蒲,處理消息的代價越大,到最后一個階段時诊赊,都創(chuàng)建了 NSInvocation 對象了厚满。
Q2:消息轉(zhuǎn)發(fā)有哪些應(yīng)用場景呢?
可以在運(yùn)行期再加入某方法,例如 Teacher 類里有teach方法碧磅,DrugDealer 類里有l(wèi)etsCook方法碘箍,通過一號接盤俠方法,我們可以在運(yùn)行期把 saleDrug 偷摸加到 teacher 的方法列表中鲸郊,讓 teacher 具備販毒的功能丰榴,[teacher? guessWhatHeDo],實(shí)際調(diào)用的是[teacher letsCook]严望,唉呀媽呀多艇,絕命毒師啊。
把方法轉(zhuǎn)給其他對象處理像吻,再舉個例子峻黍,還是 Teacher 類(博主跟老師有仇嗎...)复隆,[teacher letsCook],可以把對象在運(yùn)行期換為drugDealer姆涩。再來一個 Cook 類挽拂,也有 letsCook 方法,但這次這方法不是 cook 毒品骨饿,而是 cook 菜亏栈。因此既可以通過[teacher letsCook] 實(shí)現(xiàn)[drugDealer letsCook],也可以實(shí)現(xiàn)[cook letsCook]。相當(dāng)于 OC 實(shí)現(xiàn)了多重繼承宏赘,雖然有點(diǎn)不太恰當(dāng)...
注意
respondsToSelector我們再熟悉不過了绒北,用來檢查某對象是否實(shí)現(xiàn)了某方法。此函數(shù)通常是不需要重載的察署,但是在動態(tài)實(shí)現(xiàn)了查找過程后闷游,需要重載此函數(shù)讓對外接口查找動態(tài)實(shí)現(xiàn)函數(shù)的時候返回YES,保證對外接口的行為統(tǒng)一贴汪。
respondsToSelector
最后說一下 warning 的事脐往。編譯器很好心的報(bào)的那個 warning 咋辦呢,不管那個小黃條不是一個愛整潔的程序員的風(fēng)格扳埂,所以我們要想辦法把它去掉业簿。
有兩種方法,第一種比較暴力阳懂,通過在配置文件中把 Complier Flag 加-w梅尤,對該類去除所有 warning。
去掉所有warning
第二種是推薦的做法希太,在 xcode 的 error 面板對 warning 右鍵-Reveal in Log,這里有個小 bug克饶,如果這個選項(xiàng)不可選擇,需要你重新 build 一下就可選了誊辉,
小 Bug
在右側(cè)矾湃,可以看到這個warning 的名稱,
如何看warning名稱
所以用這個宏把出現(xiàn) warning 的代碼包圍起來堕澄,就可以讓編譯器不再報(bào)錯:
#pragmaclang diagnostic push#pragmaclang diagnostic ignored"-Wobjc-method-access"[self setText:@"你好"];#pragmaclang diagnostic pop
文/毀小慕(簡書作者)
原文鏈接:http://www.reibang.com/p/fa29c920409d
著作權(quán)歸作者所有邀跃,轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),并標(biāo)注“簡書作者”蛙紫。