在 iOS
項(xiàng)目中衰粹,我們經(jīng)常會遇到 x[xx xx]: unrecognized selector sent to instance xxx
的 crash
粱锐,調(diào)用類沒有實(shí)現(xiàn)的方法就會出現(xiàn)這個經(jīng)典的 crash
勺良,如下圖逝变,消息查找流程 這篇文章分析了如何找到報這個 crash
的原因嗤瞎,接下來我一步一步帶你分析原因以及如何避免此 crash
欧引。
一、動態(tài)方法決議
1._class_resolveMethod 分析
當(dāng)調(diào)用類沒有實(shí)現(xiàn)的方法時倔韭,先會去本類和父類等的方法列表中找該方法术浪,若沒有找到則會進(jìn)入到動態(tài)方法決議 _class_resolveMethod,也是蘋果爸爸給我們的一次防止 crash 的機(jī)會寿酌,讓我們能有更多的動態(tài)性胰苏,那又該如何防止呢,接著往下看醇疼。
_class_resolveMethod(Class cls, SEL sel, id inst)硕并,當(dāng)進(jìn)行實(shí)例方法動態(tài)解析時,cls是類秧荆,inst是實(shí)例對象倔毙,如果是進(jìn)行類方法動態(tài)解析時,cls是元類乙濒,inst是類陕赃。
if (resolver && !triedResolver) {
...
_class_resolveMethod(cls, sel, inst);
...
goto retry;
}
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
// 判斷當(dāng)前是否是元類
if (! cls->isMetaClass()) {
// 類,嘗試找實(shí)例方法
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// 是元類颁股,先找類方法
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// 為什么這里還要查找一次呢么库?下面會分析
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
在這個方法會有兩種情況,一種是對象方法決議豌蟋,另外一種是類方法決議廊散。
2.對象方法決議
/***********************************************************************
* _class_resolveInstanceMethod
* Call +resolveInstanceMethod, looking for a method to be added to class cls.
**********************************************************************/
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
// 看注釋可以得知 SEL_resolveInstanceMethod 就是 類方法resolveInstanceMethod
// 去 cls 找是否實(shí)現(xiàn)了 resolveInstanceMethod 方法
// 如果沒有實(shí)現(xiàn),則直接返回梧疲,就不會給 cls 發(fā)送 resolveInstanceMethod 消息允睹,就不會報找不到 resolveInstanceMethod
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}
// 本類實(shí)現(xiàn)了類方法 resolveInstanceMethod
// 當(dāng)對象找不到需要調(diào)用的方法時,系統(tǒng)就會主動響應(yīng) resolveInstanceMethod 方法幌氮,可以在 resolveInstanceMethod 進(jìn)行自定義處理
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
// 再次去查找方法缭受,找不到就會崩潰
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
// 省略了一些不重要的報錯信息代碼
...
}
3._class_resolveInstanceMethod 小結(jié)
1.在 _class_resolveInstanceMethod 里首先會去本類查找類方法 resolveInstanceMethod 是否實(shí)現(xiàn),如果本類沒有實(shí)現(xiàn)則直接返回空该互,如果自己實(shí)現(xiàn)了就會走到下一步米者。
2.下一步會給本類發(fā)送 msg(cls, SEL_resolveInstanceMethod, sel) 消息,而本類卻沒有實(shí)現(xiàn)宇智,但最終報的錯不是找不到 resolveInstanceMethod 方法蔓搞,所以有點(diǎn)奇怪,那是不是父類實(shí)現(xiàn)了呢随橘?通過全局搜索 resolveInstanceMethod 喂分,最終在 NSObject 里面找到這個方法的實(shí)現(xiàn),所以會走到 NSObject 的實(shí)現(xiàn)返回 NO机蔗。
3.最后會通過 lookUpImpOrNil 再次去尋找該方法的實(shí)現(xiàn)蒲祈,如果還沒找到就會崩潰甘萧。
4.因?yàn)檎麄€崩潰的原因是找不到方法實(shí)現(xiàn),所以如果我們自己在本類里實(shí)現(xiàn) resolveInstanceMethod梆掸,當(dāng)沒有找到方法實(shí)現(xiàn)最終會走到 resolveInstanceMethod 里面扬卷,在這個方法里面動態(tài)添加本類沒有實(shí)現(xiàn)的 imp,最后一次的 lookUpImpOrNil 就會找到對應(yīng)的 imp 進(jìn)行返回酸钦,這樣就不會導(dǎo)致項(xiàng)目的 crash 了怪得。
5.resolveInstanceMethod 是系統(tǒng)給我們的一次機(jī)會,讓我們可以針對沒有實(shí)現(xiàn)的 sel 進(jìn)行自定義操作钝鸽。
解決方法如下
// 由于類方法和實(shí)例方法差不多汇恤,就寫在一起了
// 實(shí)例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"來了 老弟 - %p",sel);
if (sel == @selector(saySomething)) {
NSLog(@"說話了");
IMP sayHIMP = class_getMethodImplementation(self, @selector(studentSayHello));
Method sayHMethod = class_getInstanceMethod(self, @selector(studentSayHello));
const char *sayHType = method_getTypeEncoding(sayHMethod);
return class_addMethod(self, sel, sayHIMP, sayHType);
}
return [super resolveInstanceMethod:sel];
}
// 類方法
// 類方法需要注意的一點(diǎn)是 類方法是存在元類里面的,所以添加的方法也是要添加到元類里面去
+ (BOOL)resolveClassMethod:(SEL)sel {
NSLog(@"類方法 來了 老弟 - %p",sel);
if (sel == @selector(studentSayLove)) {
NSLog(@"說你愛我");
IMP sayHIMP = class_getMethodImplementation(objc_getMetaClass("Student"), @selector(studentSayObjc));
Method sayHMethod = class_getInstanceMethod(objc_getMetaClass("Student"), @selector(studentSayObjc));
const char *sayHType = method_getTypeEncoding(sayHMethod);
return class_addMethod(objc_getMetaClass("Student"), sel, sayHIMP, sayHType);
}
return [super resolveInstanceMethod:sel];
}
3.類方法決議
_class_resolveClassMethod
和_class_resolveInstanceMethod
邏輯差不多拔恰,只不過類方法是去元類里處理因谎。
/***********************************************************************
* _class_resolveClassMethod
* Call +resolveClassMethod, looking for a method to be added to class cls.
**********************************************************************/
static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
assert(cls->isMetaClass());
// 去元類里面找 resolveClassMethod,沒有找到直接返回空
if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}
// 給類發(fā)送 resolveClassMethod 消息
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
// _class_getNonMetaClass 對元類進(jìn)行初始化準(zhǔn)備颜懊,以及判斷是否是根元類的一些判斷财岔,有興趣的可以自己去看看
bool resolved = msg(_class_getNonMetaClass(cls, inst),
SEL_resolveClassMethod, sel);
// 再次去查找方法
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
// 省略了一些不重要的報錯信息代碼
...
}
4.類方法需要解析兩次的分析
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// 為什么這里還要查找一次呢?
_class_resolveInstanceMethod(cls, sel, inst);
}
既然上面的對象方法決議和類方法決議都會走 _class_resolveInstanceMethod河爹,而最終都會找到父類 NSObject 里面去匠璧,那我們在 NSObject 分類里面重寫 resolveInstanceMethod 方法,在這個方法里面對沒有實(shí)現(xiàn)的方法(不管是類方法還是對象方法)進(jìn)行動態(tài)添加 imp咸这,然后再進(jìn)行自定義處理(比如彈個框說網(wǎng)絡(luò)不佳夷恍,在進(jìn)行后臺的bug收集),豈不是美滋滋了媳维。
NSObject+crash.m
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"來了老弟:%s - %@",__func__,NSStringFromSelector(sel));
if (sel == @selector(saySomething)) {
NSLog(@"說話了");
IMP sayHIMP = class_getMethodImplementation(self, @selector(sayMaster));
Method sayHMethod = class_getInstanceMethod(self, @selector(sayMaster));
const char *sayHType = method_getTypeEncoding(sayHMethod);
return class_addMethod(self, sel, sayHIMP, sayHType);
}
if (xx) {
// 后臺 bug 收集或者其他一些自定義處理
}
}
二酿雪、消息轉(zhuǎn)發(fā)
1.快速轉(zhuǎn)發(fā) forwardingTargetForSelector當(dāng)自己沒有進(jìn)行動態(tài)方法決議時,就會來到我們的消息轉(zhuǎn)發(fā)侄刽,那消息轉(zhuǎn)發(fā)又是怎么樣的呢指黎?通過 instrumentObjcMessageSends(true); 函數(shù)來設(shè)置是否輸出日志,且該日志存儲在/tmp/msgSends-"xx";
Student *student = [[Student alloc] init];
instrumentObjcMessageSends(true);
[student saySomething];
instrumentObjcMessageSends(false);
查看日志輸出如下:
然后通過在源碼中搜索 forwardingTargetForSelector 發(fā)現(xiàn)這個實(shí)現(xiàn)州丹,好像沒什么線索醋安,那這個時候是不是就此就結(jié)束了?不墓毒,在源碼中發(fā)現(xiàn)不了線索吓揪,我還有一個神器,官方文檔 command + shift + 0所计,搜索 forwardingTargetForSelector磺芭,官方文檔解釋的清清楚楚明明白白。
Person.m
- (void)studentSaySomething {
NSLog(@"Person-%s",__func__);
}
Student.m
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(studentSaySomething)) {
return [Person new];
}
return [super forwardingTargetForSelector:aSelector];
}
將 Student 未實(shí)現(xiàn)的方法在 Person 實(shí)現(xiàn)醉箕,然后 forwardingTargetForSelector 重定向到 Person 里钾腺,這樣也不會造成崩潰。
2.慢速轉(zhuǎn)發(fā) methodSignatureForSelector
當(dāng)我們在快速轉(zhuǎn)發(fā)的 forwardingTargetForSelector 沒有進(jìn)行處理或者重定向的對象也沒有處理讥裤,則會來到慢速轉(zhuǎn)發(fā)的 methodSignatureForSelector放棒。通過查看官方文檔,methodSignatureForSelector 還要搭配 forwardInvocation 方法一起使用己英,具體的可以自行去官方文檔查看间螟。
methodSignatureForSelector:返回 sel 的方法簽名,返回的簽名是根據(jù)方法的參數(shù)來封裝的损肛。這個函數(shù)讓重載方有機(jī)會拋出一個函數(shù)的簽名厢破,再由后面的 forwardInvocation 去執(zhí)行。
forwardInvocation:可以將 NSInvocation 多次轉(zhuǎn)發(fā)到多個對象治拿。
Person.m
- (void)studentSaySomething {
NSLog(@"Person-%s",__func__);
}
Teacher.m
- (void)studentSaySomething {
NSLog(@"Person-%s",__func__);
}
Student.m
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"Student-%s",__func__);
// 判斷selector是否為需要轉(zhuǎn)發(fā)的摩泪,如果是則手動生成方法簽名并返回。
if (aSelector == @selector(studentSaySomething)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"Student-%s",__func__);
// SEL aSelector = [anInvocation selector];
// if ([[Person new] respondsToSelector:aSelector])
// [anInvocation invokeWithTarget:[Person new]];
// else
// [super forwardInvocation:anInvocation];
// if ([[Teacher new] respondsToSelector:aSelector])
// [anInvocation invokeWithTarget:[Teacher new]];
// else
// [super forwardInvocation:anInvocation];
}
如果 forwardInvocation 什么都沒做的話劫谅,僅僅只是 methodSignatureForSelector 返回了簽名见坑,則什么也不會發(fā)生,也不會崩潰捏检。
慢速轉(zhuǎn)發(fā)和快速轉(zhuǎn)發(fā)比較類似荞驴,都是將A類的某個方法,轉(zhuǎn)發(fā)到B 類的實(shí)現(xiàn)中去贯城。不同的是熊楼,forwardInvocation 的轉(zhuǎn)發(fā)相對更加靈活,forwardingTargetForSelector 只能固定的轉(zhuǎn)發(fā)到一個對象能犯,forwardInvocation 可以讓我們轉(zhuǎn)發(fā)到多個對象中去鲫骗。
3.消息無法處理 doesNotRecognizeSelector
// 報出異常錯誤
- (void)doesNotRecognizeSelector:(SEL)sel {
_objc_fatal("-[%s %s]: unrecognized selector sent to instance %p",
object_getClassName(self), sel_getName(sel), self);
}
三、總結(jié)
1.當(dāng)動態(tài)方法決議resolveInstanceMethod
返回 NO
悲雳,就會來到 forwardingTargetForSelector:
挎峦,獲取新的 target
作為receiver
重新執(zhí)行 selector
,如果返回nil或者返回的對象沒有處理合瓢,進(jìn)入第二步坦胶。
2.methodSignatureForSelector
獲取方法簽名后,判斷返回類型信息是否正確晴楔,再調(diào)用 forwardInvocation
執(zhí)行 NSInvocation
對象顿苇,并將結(jié)果返回。如果對象沒有實(shí)現(xiàn)methodSignatureForSelector
税弃,進(jìn)入第三步纪岁。
3.doesNotRecognizeSelector:
拋出異常 unrecognized selector sent to instance %p
。
下面附上我總結(jié)的圖