iOS中對unrecognized selector的防御

在 iOS 開發(fā)中真仲,App的崩潰原因有很多種,這篇文章主要闡述我所使用的防止發(fā)送未知消息(unrecognized selector)**導致崩潰的方法及思路歪沃,希望能起到拋磚引玉的作用。若有錯誤,歡迎指出硅堆!

unrecognized selector sent to instance 0x7faa2a132c0

調試過程中如果看到輸出這句話,我們馬上就能知道某個對象并沒有實現(xiàn)向他發(fā)送的消息贿讹。如果是在已經(jīng)上線的版本中發(fā)現(xiàn)的……GAME OVER...(你也可以用熱修復)

消息發(fā)送的機制我們都明白渐逃,通過superclass指針逐級向上查找該消息所對應的方法實現(xiàn)。如果直到根類都沒有找到這個方法的實現(xiàn)民褂,運行時會通過補救機制茄菊,繼續(xù)嘗試查找方法的實現(xiàn)疯潭。那么我們能不能通過重寫其中的某個方法,來達到不崩潰的目的面殖?

我們先了解下這個補救機制:

runtime_sendMsg.png

直到最后一步消息無法處理后袁勺,我們的App就崩潰了,隨后我們就看到了熟悉的unrecognized selector...
這些方法究竟能做什么畜普,我們來看看蘋果官方的描述(我對其中比較重要的部分翻譯了一下):

resolveInstanceMethod:

resolveInstanceMethod:resolveClassMethod: 方法允許你為一個給定的 selector 動態(tài)的提供方法的實現(xiàn)期丰。
OC 方法在底層的C函數(shù)的實現(xiàn)中需要至少兩個參數(shù):self 和 _cmd。使用** class_addMethod **函數(shù)吃挑,你能夠添加一個函數(shù)到一個類來作為方法使用钝荡。

** forwardingTargetForSelector:**

如果一個對象實現(xiàn)了這個方法,并且返回了一個非空(以及非 self)的結果舶衬,返回的對象會用來作為一個新的接收對象埠通,隨后消息會被重新派發(fā)給這個新對象。(很明顯逛犹,如果你在這個方法中返回了self端辱,那這段代碼將會墜入無限循環(huán)。)
如果你這段方法在一個非 root 的類中實現(xiàn)虽画,并且如果這個類根據(jù)給定的selector什么都不作返回舞蔽,那么你應該返回一個 執(zhí)行父類的實現(xiàn)后返回的結果。

這個方法為對象在開銷大的多的 forwardInvocation: 方法接管之前提供了一次轉發(fā)未知消息的機會码撰。這對你只是想簡單的重新定位消息到另一個對象是非常有用的渗柿,并且相對普通轉發(fā)更快一個數(shù)量級。如果轉發(fā)的目的是捕捉到NSInvocation脖岛,或者操作參數(shù)朵栖,亦或者是在轉發(fā)過程中返回一個值,那這個方法就沒有用了柴梆。

** forwardInvocation: **

當對象接受到一條自己不能響應的消息時陨溅,運行時會給接收者一次機會來把消息委托給另一個接收者。他委托的消息是通過NSInvocation對象來表示的绍在,然后將這個對象作為** forwardInvocation: 的參數(shù)门扇。接收者收到 forwardInvocation: **這條消息后可以選擇轉發(fā)這個NSInvacation對象給其他接收對象。(如果這個接收對象也不能響應這條消息揣苏,他也會給一次轉發(fā)這條消息的機會悯嗓。)

因此 forwardInvocation: 允許在兩個對象之間通過某個消息來建立關系。轉發(fā)給其他對象的這種行為卸察,從某種意義上來說脯厨,他“繼承”了他所轉發(fā)給的對象的一些特征。

注意
為了響應這個你無法識別的方法坑质,你除了 forwardInvocation: 方法外合武,還必須重寫 methodSignatureForSelector: ** 方法临梗。在轉發(fā)消息的機制中會從 methodSignatureForSelector: **方法來創(chuàng)建NSInvocation對象。所以你必須為給定的 selector 提供一個合適的 method signature 稼跳,可以通過預先設置一個或者向另一個對象請求一個盟庞。

以上,是蘋果官方文檔對這三個關鍵方法的解釋汤善。

簡而言之:

  • **resolveInstanceMethod: ** 會為對象或類新增一個方法什猖。如果此時這個類是個系統(tǒng)原生的類,比如 NSArray 红淡,你向他發(fā)送了一條 setValue: forKey: 的方法不狮,這本身就是一次錯發(fā)。此時如果你為他添加這個方法在旱,這個方法一般來說就是冗余的摇零。

  • ** forwardInvocation: ** 必須要經(jīng)過 methodSignatureForSelector: ** 方法來獲得一個NSInvocation,開銷比較大桶蝎。蘋果在 forwardingTargetForSelector **的discussion中也說這個方法是一個相對開銷多的多的方法驻仅。

  • ** forwardingTargetForSelector: ** 這個方法目的單純,就是轉發(fā)給另一個對象登渣,別的他什么都不干噪服,相對以上兩個方法,更適合重寫绍豁。

既然** forwardingTargetForSelector: **方法能夠轉發(fā)給別其他對象芯咧,那我們可以創(chuàng)建一個類,所有的沒查找到的方法全部轉發(fā)給這個類竹揍,由他來動態(tài)的實現(xiàn)。而這個類中應該有一個安全的實現(xiàn)方法來動態(tài)的代替原方法的實現(xiàn)邪铲。

整理下思路:

  1. 創(chuàng)建一個接收未知消息的類芬位,暫且稱之為 Protector
  2. 創(chuàng)建一個 NSObject 的分類
  3. 在分類中重寫** forwardingTargetForSelector: **,在這個方法中截獲未實現(xiàn)的方法带到,轉發(fā)給 Protector昧碉。并為 Protector 動態(tài)的添加未實現(xiàn)的方法,最后返回 Protector 的實例對象揽惹。
  4. 在分類中新增一個安全的方法實現(xiàn)被饿,來作為 Protector 接收到的未知消息的實現(xiàn)

上代碼:

創(chuàng)建一個Protector類,沒必要new文件出來搪搏,動態(tài)生成一個就可以了狭握。注意,如果這個方法被執(zhí)行到兩次疯溺,連續(xù)兩次創(chuàng)建同一個類一定會崩潰论颅,所以我們要加一層判斷:

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    
    Class protectorCls = NSClassFromString(@"Protector");
    if (!protectorCls)
    {
        protectorCls = objc_allocateClassPair([NSObject class], "Protector", 0);
        objc_registerClassPair(protectorCls);
    }
}

然后我們要為這個類添加方法哎垦,在添加方法之前我們也要做一層判斷,是否已經(jīng)添加過這個方法(此處文末有更新說明)

        NSString *selectorStr = NSStringFromSelector(aSelector);
        // 檢查類中是否存在該方法恃疯,不存在則添加
        if (![self isExistSelector:aSelector inClass:protectorCls])
        {
            class_addMethod(protectorCls, aSelector, [self safeImplementation:aSelector],
                            [selectorStr UTF8String]);
        }

這里面有一個** safeImplementation: **方法漏设,其實就是生成一個IMP,然后返回今妄。這里我只是簡單的輸出一句話:

// 一個安全的方法實現(xiàn)
- (IMP)safeImplementation:(SEL)aSelector
{
    IMP imp = imp_implementationWithBlock(^()
    {
        NSLog(@"PROTECTOR: %@ Done", NSStringFromSelector(aSelector));
    });
    return imp;
}

isExistSelector: inClass:的實現(xiàn)代碼如下郑口,主要是根據(jù)給定的selector在class中查找,如果找到對應的實現(xiàn)則返回YES:

// 判斷某個class中是否存在某個SEL
- (BOOL)isExistSelector: (SEL)aSelector inClass:(Class)currentClass
{
    BOOL isExist = NO;
    unsigned int methodCount = 0;
    Method *methods = class_copyMethodList(currentClass, &methodCount);
    
    for (int i = 0; i < methodCount; i++)
    {
        Method temp = methods[i];
        SEL sel = method_getName(temp);
        NSString *methodName = NSStringFromSelector(sel);
        if ([methodName isEqualToString: NSStringFromSelector(aSelector)])
        {
            isExist = YES;
            break;
        }
    }
    return isExist;
}

回到我們的** forwardingTargetForSelector: **方法盾鳞,接下來就該返回Protector的實例了:

        Class Protector = [protectorCls class];
        id instance = [[Protector alloc] init];
        
        return instance;

但是經(jīng)過測試潘酗,目前的代碼還有個問題:App啟動時有些系統(tǒng)方法也會經(jīng)由這個方法轉發(fā)對象,啟動完成就不存在這種問題雁仲。所以我們在** forwardingTargetForSelector: **方法中要再加一次判斷仔夺,如果 self 是我們所關心的類,我們才轉發(fā)對象攒砖,否則返回nil缸兔。
以下是 **forwardTargetForSelector: **完整的代碼,這里我關心的是UIResponder 和 NSNull這兩個類(你也可以添加諸如NSArray\NSDictionary等類):

// 重寫消息轉發(fā)方法
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    NSString *selectorStr = NSStringFromSelector(aSelector);
    // 做一次類的判斷吹艇,只對 UIResponder 和 NSNull 有效
    if ([[self class] isSubclassOfClass: NSClassFromString(@"UIResponder")] ||
        [self isKindOfClass: [NSNull class]])
    {
        NSLog(@"PROTECTOR: -[%@ %@]", [self class], selectorStr);
        NSLog(@"PROTECTOR: unrecognized selector \"%@\" sent to instance: %p", selectorStr, self);
        // 查看調用棧
        NSLog(@"PROTECTOR: call stack: %@", [NSThread callStackSymbols]);

        // 對保護器插入該方法的實現(xiàn)
        Class protectorCls = NSClassFromString(@"Protector");
        if (!protectorCls)
        {
            protectorCls = objc_allocateClassPair([NSObject class], "Protector", 0);
            objc_registerClassPair(protectorCls);
        }
        
        // 檢查類中是否存在該方法惰蜜,不存在則添加
        if (![self isExistSelector:aSelector inClass:protectorCls])
        {
            class_addMethod(protectorCls, aSelector, [self safeImplementation:aSelector],
                            [selectorStr UTF8String]);
        }
        
        Class Protector = [protectorCls class];
        id instance = [[Protector alloc] init];
        
        return instance;
    }
    else
    {
        return nil;
    }
}

以上就是所有代碼(所以我就不上傳DEMO了)。

實驗結果:

試驗中受神,我對一個label perform了一個未知的方法:callMeTryTry抛猖,由于他是一個UIRespnder的子類,所以會進入調用我們的 Protector鼻听〔浦控制臺輸出如下,并且沒有崩潰撑碴。(所有日志不是真的崩潰時候的日志撑教,前面都帶有 PROTECTOR 字樣,全都是我代碼里的輸出)醉拓,你也可以不進行類的判斷試一下伟姐,你會看到很多這樣的輸出。

console_log.png

以上就是本文全部亿卤,希望對各位有幫助愤兵,有問題也可以互相交流。

20170214 更新:
class_addMethod 方法之前排吴,其實不需要判斷是否已添加過這個方法秆乳。因為蘋果官方文檔說 class_addMethod 方法只會覆蓋父類的方法,或者不存在的方法傍念。如果是已經(jīng)存在的方法矫夷,他不會重復添加或替代葛闷。
所以** - (BOOL)isExistSelector: (SEL)aSelector inClass:(Class)currentClass **可以不要了。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末双藕,一起剝皮案震驚了整個濱河市淑趾,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌忧陪,老刑警劉巖扣泊,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異嘶摊,居然都是意外死亡延蟹,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門叶堆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來阱飘,“玉大人,你說我怎么就攤上這事虱颗×ば伲” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵忘渔,是天一觀的道長高帖。 經(jīng)常有香客問我,道長畦粮,這世上最難降的妖魔是什么散址? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮宣赔,結果婚禮上预麸,老公的妹妹穿的比我還像新娘。我一直安慰自己拉背,他們只是感情好师崎,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著椅棺,像睡著了一般。 火紅的嫁衣襯著肌膚如雪齐蔽。 梳的紋絲不亂的頭發(fā)上两疚,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機與錄音含滴,去河邊找鬼诱渤。 笑死,一個胖子當著我的面吹牛谈况,可吹牛的內(nèi)容都是我干的勺美。 我是一名探鬼主播递胧,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼赡茸!你這毒婦竟也來了缎脾?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤占卧,失蹤者是張志新(化名)和其女友劉穎遗菠,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體华蜒,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡辙纬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了叭喜。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片贺拣。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖捂蕴,靈堂內(nèi)的尸體忽然破棺而出譬涡,到底是詐尸還是另有隱情,我是刑警寧澤启绰,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布昂儒,位于F島的核電站,受9級特大地震影響委可,放射性物質發(fā)生泄漏渊跋。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一着倾、第九天 我趴在偏房一處隱蔽的房頂上張望拾酝。 院中可真熱鬧,春花似錦卡者、人聲如沸蒿囤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽材诽。三九已至,卻和暖如春恒傻,著一層夾襖步出監(jiān)牢的瞬間脸侥,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工盈厘, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留睁枕,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像外遇,于是被迫代替她去往敵國和親注簿。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

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