在 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)疯潭。那么我們能不能通過重寫其中的某個方法,來達到不崩潰的目的面殖?
我們先了解下這個補救機制:
直到最后一步消息無法處理后袁勺,我們的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)邪铲。
整理下思路:
- 創(chuàng)建一個接收未知消息的類芬位,暫且稱之為 Protector
- 創(chuàng)建一個 NSObject 的分類
- 在分類中重寫** forwardingTargetForSelector: **,在這個方法中截獲未實現(xiàn)的方法带到,轉發(fā)給 Protector昧碉。并為 Protector 動態(tài)的添加未實現(xiàn)的方法,最后返回 Protector 的實例對象揽惹。
- 在分類中新增一個安全的方法實現(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 字樣,全都是我代碼里的輸出)醉拓,你也可以不進行類的判斷試一下伟姐,你會看到很多這樣的輸出。
以上就是本文全部亿卤,希望對各位有幫助愤兵,有問題也可以互相交流。
20170214 更新:
class_addMethod 方法之前排吴,其實不需要判斷是否已添加過這個方法秆乳。因為蘋果官方文檔說 class_addMethod 方法只會覆蓋父類的方法,或者不存在的方法傍念。如果是已經(jīng)存在的方法矫夷,他不會重復添加或替代葛闷。
所以** - (BOOL)isExistSelector: (SEL)aSelector inClass:(Class)currentClass **可以不要了。