Crash攔截器 - 讓unrecognized selector消失

在本文中,我們將了解到如下內(nèi)容:

  1. 基礎(chǔ)的消息轉(zhuǎn)發(fā)流程
  2. unrecognized selector 攔截建議
  3. 快速轉(zhuǎn)發(fā)(Fast Forwarding)攔截unrecognized selector
  4. 常規(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è)步驟:

  1. 動(dòng)態(tài)方法解析:resolveInstanceMethod:resolveClassMethod:
  2. 消息轉(zhuǎn)發(fā)
    • 快速轉(zhuǎn)發(fā):forwardingTargetForSelector:
    • 常規(guī)轉(zhuǎn)發(fā):methodSignatureForSelector:forwardInvocation:

消息轉(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è)方法中返回selfnil凑阶,則表示沒(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)方法解析 - 不建議
    1. 這個(gè)方法會(huì)為類(lèi)添加本身不存在的方法宝冕,絕大多數(shù)情況下,這個(gè)方法時(shí)冗余的邓萨。
    2. 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)需要提一下:

  1. - (id)forwardingTargetForSelector:(SEL)aSelector;+ (id)forwardingTargetForSelector:(SEL)aSelector;都要在NSObject的分類(lèi)中重寫(xiě)卓缰。前者對(duì)應(yīng)實(shí)例方法,后者對(duì)應(yīng)類(lèi)方法。
  2. 過(guò)濾掉一些系統(tǒng)內(nèi)部對(duì)象征唬,否則在啟動(dòng)的時(shí)候就會(huì)有一些奇怪的異常被捕獲到捌显。
  3. 我們需要判斷當(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)行攔截。
  4. 細(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è)返回值為voidNSMethodSignature毫别,在forwardInvocation:中我們不做任何事情。這樣將性能消耗減到最小典格。

以上:我們可以選擇其中一種方式來(lái)實(shí)現(xiàn)我們對(duì)unrecognized selector的攔截,跟unrecognized selector徹底說(shuō)拜拜啦(手動(dòng)微笑)台丛。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末耍缴,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子挽霉,更是在濱河造成了極大的恐慌防嗡,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件侠坎,死亡現(xiàn)場(chǎng)離奇詭異蚁趁,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)实胸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)他嫡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人庐完,你說(shuō)我怎么就攤上這事钢属。” “怎么了门躯?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵淆党,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng)染乌,這世上最難降的妖魔是什么山孔? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮荷憋,結(jié)果婚禮上台颠,老公的妹妹穿的比我還像新娘。我一直安慰自己台谊,他們只是感情好蓉媳,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著锅铅,像睡著了一般酪呻。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上盐须,一...
    開(kāi)封第一講書(shū)人閱讀 51,365評(píng)論 1 302
  • 那天玩荠,我揣著相機(jī)與錄音,去河邊找鬼贼邓。 笑死阶冈,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的塑径。 我是一名探鬼主播女坑,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼统舀!你這毒婦竟也來(lái)了匆骗?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤誉简,失蹤者是張志新(化名)和其女友劉穎碉就,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體闷串,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡瓮钥,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了烹吵。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片碉熄。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖年叮,靈堂內(nèi)的尸體忽然破棺而出具被,到底是詐尸還是另有隱情,我是刑警寧澤只损,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布一姿,位于F島的核電站七咧,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏叮叹。R本人自食惡果不足惜艾栋,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蛉顽。 院中可真熱鬧蝗砾,春花似錦、人聲如沸携冤。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)曾棕。三九已至扣猫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間翘地,已是汗流浹背申尤。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留衙耕,地道東北人昧穿。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像橙喘,于是被迫代替她去往敵國(guó)和親时鸵。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

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