在本文中,我們將了解到如下內(nèi)容:
- 基礎(chǔ)的消息轉(zhuǎn)發(fā)流程
- unrecognized selector 攔截建議
- 快速轉(zhuǎn)發(fā)(Fast Forwarding)攔截unrecognized selector
- 常規(guī)轉(zhuǎn)發(fā)(Normal Forwarding)攔截unrecognized selector
前言
我們?cè)诘谝惶鞂W(xué)習(xí)Objective-C
這一門(mén)語(yǔ)言的時(shí)候侵蒙,就被告知這是一門(mén)動(dòng)態(tài)語(yǔ)言铣卡。C
這樣的編譯語(yǔ)言,在編譯階段就確定了所有函數(shù)的調(diào)用鏈姥闪,如果函數(shù)沒(méi)有被實(shí)現(xiàn)忌警,編譯就根本不過(guò)了营搅。而基于動(dòng)態(tài)語(yǔ)言的特性俐镐,在編譯期間,我們無(wú)法確認(rèn)程序在運(yùn)行時(shí)要調(diào)用哪一個(gè)函數(shù)哺哼,某一個(gè)未被實(shí)現(xiàn)的函數(shù)是否會(huì)在運(yùn)行時(shí)被實(shí)現(xiàn)佩抹。
這樣就可能會(huì)出現(xiàn)運(yùn)行時(shí)發(fā)現(xiàn)調(diào)用的函數(shù)根本不存在的尷尬,這也就是我們收到unrecognized selector sent to XXX
這樣的崩潰的原因了(動(dòng)態(tài)語(yǔ)言也有讓人心累的地方取董,手動(dòng)嘆氣)棍苹。
這篇文章要討論的就是如果遇到了這種尷尬情況的時(shí)候,我們?cè)撊绾伪苊馕覀冏钭钭钣憛挼谋罎?是的茵汰,所有的崩潰都是最最最讓人討厭的)枢里。
消息轉(zhuǎn)發(fā)流程
我們知道在我們調(diào)用某一個(gè)方法之后,最終調(diào)用的是objc_msgSend()
這樣一個(gè)方法蹂午,發(fā)送消息
(selector
)給消息接收者
(receiver
)栏豺。這個(gè)方法會(huì)根據(jù)OC
的消息發(fā)送機(jī)制在receiver
中查找selector
。如果沒(méi)有查找到豆胸,就會(huì)出現(xiàn)上述的運(yùn)行時(shí)調(diào)用了未實(shí)現(xiàn)的函數(shù)的尷尬局面了奥洼。
不過(guò)為了緩解這種尷尬,我們還有機(jī)會(huì)來(lái)掙扎晚胡。這掙扎機(jī)會(huì)就是消息轉(zhuǎn)發(fā)流程灵奖。
消息轉(zhuǎn)發(fā)流程包含以下3個(gè)步驟:
- 動(dòng)態(tài)方法解析:
resolveInstanceMethod:
和resolveClassMethod:
- 消息轉(zhuǎn)發(fā)
- 快速轉(zhuǎn)發(fā):
forwardingTargetForSelector:
- 常規(guī)轉(zhuǎn)發(fā):
methodSignatureForSelector:
和forwardInvocation:
- 快速轉(zhuǎn)發(fā):
消息轉(zhuǎn)發(fā)流程是以動(dòng)態(tài)方法解析、消息快速轉(zhuǎn)發(fā)估盘、消息常規(guī)轉(zhuǎn)發(fā)這樣的順序來(lái)執(zhí)行的瓷患。如果其中任意一個(gè)步驟能使消息被執(zhí)行,那么就不會(huì)出現(xiàn)unrecognized selector sent to XXX
的崩潰遣妥。
動(dòng)態(tài)方法解析
resolveInstanceMethod:
這個(gè)方法的作用是動(dòng)態(tài)地為selector
提供一個(gè)實(shí)例方法的實(shí)現(xiàn)擅编。而resolveClassMethod:
則是提供一個(gè)類(lèi)方法的實(shí)現(xiàn)。
所以我們可以在這兩個(gè)方法中燥透,為對(duì)象添加方法的實(shí)現(xiàn)沙咏,再返回YES
告訴已經(jīng)為selector
添加了實(shí)現(xiàn)辨图。這樣就會(huì)重新在對(duì)象上查找方法,找到我們新添加的方法后就直接調(diào)用肢藐。從而避免掉unrecognized selector sent to XXX
故河。
需要注意的是: 這兩個(gè)方法會(huì)響應(yīng)respondsToSelector:
和instancesRespondToSelector:
。
消息快速轉(zhuǎn)發(fā)
forwardingTargetForSelector:
的作用是將消息轉(zhuǎn)發(fā)給其它對(duì)象去處理吆豹。
我們可以在這個(gè)方法中鱼的,返回一個(gè)對(duì)象,讓這個(gè)對(duì)象來(lái)響應(yīng)消息痘煤。
需要注意的是: 如果在這個(gè)方法中返回self
或nil
凑阶,則表示沒(méi)有可響應(yīng)的目標(biāo)。
消息常規(guī)轉(zhuǎn)發(fā)
forwardInvocation:
的作用也是將消息轉(zhuǎn)發(fā)給其它對(duì)象衷快。不過(guò)與 消息快速轉(zhuǎn)發(fā) 不同的是該方法需要手動(dòng)的創(chuàng)建一個(gè)NSInvocation
對(duì)象宙橱,并手動(dòng)地將新消息發(fā)送給新的接收者。
很顯然蘸拔,這種方式會(huì)比 消息快速轉(zhuǎn)發(fā) 付出更大的消耗师郑。
如何選擇攔截方案的建議
對(duì)于以上的三個(gè)步驟,我們?cè)撨x擇哪一個(gè)步驟來(lái)進(jìn)行攔截呢调窍?
-
動(dòng)態(tài)方法解析 - 不建議
- 這個(gè)方法會(huì)為類(lèi)添加本身不存在的方法宝冕,絕大多數(shù)情況下,這個(gè)方法時(shí)冗余的邓萨。
-
respondsToSelector:
和instancesRespondToSelector:
這兩個(gè)方法都會(huì)調(diào)用到resolveInstanceMethod:
地梨,那么在我們需要使用這兩個(gè)方法進(jìn)行判斷的時(shí)候,就會(huì)出現(xiàn)我們不想看到的情況缔恳。
-
消息快速轉(zhuǎn)發(fā) - 推薦
會(huì)攔截掉已經(jīng)通過(guò)消息常規(guī)轉(zhuǎn)發(fā)實(shí)現(xiàn)的消息轉(zhuǎn)發(fā)宝剖,但是可以通過(guò)判斷避開(kāi)對(duì)NSObject
子類(lèi)的消息常規(guī)轉(zhuǎn)發(fā)的攔截。 -
消息常規(guī)轉(zhuǎn)發(fā) - 推薦
這一步不會(huì)對(duì)原有的消息轉(zhuǎn)發(fā)機(jī)制產(chǎn)生影響褐耳,缺點(diǎn)是更大的性能開(kāi)銷(xiāo)诈闺。
快速轉(zhuǎn)發(fā)攔截方案
我們可以創(chuàng)建一個(gè)例如:crashPreventor
的類(lèi),在forwardingTargetForSelector:
中為crashPreventor
添加selector
铃芦,最后返回crashPreventor
的實(shí)例雅镊。從而讓crashPreventor
的實(shí)例響應(yīng)這個(gè)selector
。具體代碼如下:
@implementation NSObject (SelectorPreventor)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
- (id)forwardingTargetForSelector:(SEL)aSelector{
Class rootClass = NSObject.class;
Class currentClass = self.class;
return [self.class actionForwardingTargetForSelector:aSelector rootClass:rootClass currentClass:currentClass];
}
+ (id)forwardingTargetForSelector:(SEL)aSelector {
Class rootClass = objc_getMetaClass(class_getName(NSObject.class));
Class currentClass = objc_getMetaClass(class_getName(self.class));
return [self actionForwardingTargetForSelector:aSelector rootClass:rootClass currentClass:currentClass];
}
+ (id)actionForwardingTargetForSelector:(SEL)aSelector rootClass:(Class)rootClass currentClass:(Class)currentClass {
// 過(guò)濾掉內(nèi)部對(duì)象
NSString *className = NSStringFromClass(currentClass);
if ([className hasPrefix:@"_"]) {
return nil;
}
SEL methodSignatureSelector = @selector(methodSignatureForSelector:);
IMP rootMethodSignatureMethod = class_getMethodImplementation(rootClass, methodSignatureSelector);
IMP currentMethodSignatureMethod = class_getMethodImplementation(currentClass, methodSignatureSelector);
if (rootMethodSignatureMethod != currentMethodSignatureMethod) {
return nil;
}
NSString * selectorName = NSStringFromSelector(aSelector);
// 上報(bào)異常
// unrecognized selector sent to class XXX
// unrecognized selector sent to instance XXX
NSLog(@"unrecognized selector crash:%@:%@", className, selectorName);
// 創(chuàng)建crashPreventor類(lèi)
NSString *targetClassName = @"crashPreventor";
Class cls = NSClassFromString(targetClassName);
if (!cls) {
// 如果要注冊(cè)類(lèi)刃滓,則必須要先判斷class是否已經(jīng)存在仁烹,否則會(huì)產(chǎn)生崩潰
// 如果不注冊(cè)類(lèi),則可以重復(fù)創(chuàng)建class
cls = objc_allocateClassPair(NSObject.class, targetClassName.UTF8String, 0);
objc_registerClassPair(cls);
}
// 如果類(lèi)沒(méi)有對(duì)應(yīng)的方法咧虎,則動(dòng)態(tài)添加一個(gè)
if (!class_getInstanceMethod(cls, aSelector)) {
Method method = class_getInstanceMethod(currentClass, @selector(crashPreventor));
class_addMethod(cls, aSelector, method_getImplementation(method), method_getTypeEncoding(method));
}
return [cls new];
}
#pragma clang diagnostic pop
- (id)crashPreventor {
return nil;
}
@end
這里有幾個(gè)點(diǎn)需要提一下:
-
- (id)forwardingTargetForSelector:(SEL)aSelector;
和+ (id)forwardingTargetForSelector:(SEL)aSelector;
都要在NSObject
的分類(lèi)中重寫(xiě)卓缰。前者對(duì)應(yīng)實(shí)例方法,后者對(duì)應(yīng)類(lèi)方法。 - 過(guò)濾掉一些系統(tǒng)內(nèi)部對(duì)象征唬,否則在啟動(dòng)的時(shí)候就會(huì)有一些奇怪的異常被捕獲到捌显。
- 我們需要判斷當(dāng)前類(lèi)是否實(shí)現(xiàn)了
methodSignatureForSelector:
方法,如果實(shí)現(xiàn)了該方法总寒,就認(rèn)為當(dāng)前類(lèi)已經(jīng)實(shí)現(xiàn)了自己的消息轉(zhuǎn)發(fā)機(jī)制扶歪,我們不對(duì)其進(jìn)行攔截。 - 細(xì)心的我們肯定有注意到摄闸,不管是類(lèi)方法還是實(shí)例方法善镰,我們都是向
crashPreventor
中添加實(shí)例方法。這是因?yàn)槟暾恚覀兊捻憫?yīng)對(duì)象時(shí)crashPreventor
實(shí)例炫欺,而selector
不區(qū)分實(shí)例方法還是類(lèi)方法。我們這么處理最終對(duì)方法執(zhí)行來(lái)說(shuō)不會(huì)有什么差別熏兄。
常規(guī)轉(zhuǎn)發(fā)攔截方案
實(shí)現(xiàn)比較簡(jiǎn)單品洛,我們直接上代碼:
@implementation NSObject (SelectorPreventor)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [NSMethodSignature signatureWithObjCTypes:"@"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"forwardInvocation------");
}
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [NSMethodSignature signatureWithObjCTypes:"@"];
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"forwardInvocation------");
}
#pragma clang diagnostic pop
@end
同樣的,類(lèi)方法和實(shí)例方法我們都需要重寫(xiě)摩桶。
在methodSignatureForSelector:
中我們返回一個(gè)返回值為void
的NSMethodSignature
毫别,在forwardInvocation:
中我們不做任何事情。這樣將性能消耗減到最小典格。
以上:我們可以選擇其中一種方式來(lái)實(shí)現(xiàn)我們對(duì)unrecognized selector
的攔截,跟unrecognized selector
徹底說(shuō)拜拜啦(手動(dòng)微笑)台丛。