在 iOS
項目中,咱們常常會遇到 x[xx xx]: unrecognized selector sent to instance xxx
的 crash
,調用類沒有實現的方法就會出現這個經典的 crash
,以下圖,消息查找流程 這篇文章分析瞭如何找到報這個 crash
的緣由,接下來我一步一步帶你分析緣由以及如何避免此 crash
。 bash
當調用類沒有實現的方法時,先會去本類和父類等的方法列表中找該方法,若沒有找到則會進入到動態方法決議 _class_resolveMethod
,也是蘋果爸爸給咱們的一次防止 crash
的機會,讓咱們能有更多的動態性,那又該如何防止呢,接着往下看。網絡
_class_resolveMethod(Class cls, SEL sel, id inst),當進行實例方法動態解析時,cls是類,inst是實例對象,若是是進行類方法動態解析時,cls是元類,inst是類。 函數
if (resolver && !triedResolver) {
...
_class_resolveMethod(cls, sel, inst);
...
goto retry;
}
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
// 判斷當前是不是元類
if (! cls->isMetaClass()) {
// 類,嘗試找實例方法
_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);
}
}
}
複製代碼
在這個方法會有兩種狀況,一種是對象方法決議,另一種是類方法決議。oop
/***********************************************************************
* _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 找是否實現了 resolveInstanceMethod 方法
// 若是沒有實現,則直接返回,就不會給 cls 發送 resolveInstanceMethod 消息,就不會報找不到 resolveInstanceMethod
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}
// 本類實現了類方法 resolveInstanceMethod
// 當對象找不到須要調用的方法時,系統就會主動響應 resolveInstanceMethod 方法,能夠在 resolveInstanceMethod 進行自定義處理
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*/);
// 省略了一些不重要的報錯信息代碼
...
}
複製代碼
_class_resolveInstanceMethod
裏首先會去本類查找類方法 resolveInstanceMethod
是否實現,若是本類沒有實現則直接返回空,若是本身實現了就會走到下一步。msg(cls, SEL_resolveInstanceMethod, sel)
消息,而本類卻沒有實現,但最終報的錯不是找不到 resolveInstanceMethod
方法,因此有點奇怪,那是否是父類實現了呢?經過全局搜索 resolveInstanceMethod
,最終在 NSObject
裏面找到這個方法的實現,因此會走到 NSObject
的實現返回 NO
。lookUpImpOrNil
再次去尋找該方法的實現,若是還沒找到就會崩潰。resolveInstanceMethod
,當沒有找到方法實現最終會走到 resolveInstanceMethod
裏面,在這個方法裏面動態添加本類沒有實現的 imp
,最後一次的 lookUpImpOrNil
就會找到對應的 imp
進行返回,這樣就不會致使項目的 crash
了。resolveInstanceMethod
是系統給咱們的一次機會,讓咱們能夠針對沒有實現的 sel
進行自定義操做。// 因爲類方法和實例方法差很少,就寫在一塊兒了
// 實例方法
+ (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];
}
// 類方法
// 類方法須要注意的一點是 類方法是存在元類裏面的,因此添加的方法也是要添加到元類裏面去
+ (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];
}
複製代碼
_class_resolveClassMethod
和 _class_resolveInstanceMethod
邏輯差很少,只不過類方法是去元類裏處理。post
/***********************************************************************
* _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;
}
// 給類發送 resolveClassMethod 消息
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
// _class_getNonMetaClass 對元類進行初始化準備,以及判斷是不是根元類的一些判斷,有興趣的能夠本身去看看
bool resolved = msg(_class_getNonMetaClass(cls, inst),
SEL_resolveClassMethod, sel);
// 再次去查找方法
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
// 省略了一些不重要的報錯信息代碼
...
}
複製代碼
_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
方法,在這個方法裏面對沒有實現的方法(不論是類方法仍是對象方法)進行動態添加 imp
,而後再進行自定義處理(好比彈個框說網絡不佳,在進行後臺的bug收集),豈不是美滋滋了。ui
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 收集或者其餘一些自定義處理
}
}
複製代碼
當本身沒有進行動態方法決議時,就會來到咱們的消息轉發,那消息轉發又是怎麼樣的呢?經過 instrumentObjcMessageSends(true);
函數來設置是否輸出日誌,且該日誌存儲在/tmp/msgSends-"xx"
;this
Student *student = [[Student alloc] init];
instrumentObjcMessageSends(true);
[student saySomething];
instrumentObjcMessageSends(false);
複製代碼
查看日誌輸出以下:spa
forwardingTargetForSelector
發現這個實現,好像沒什麼線索,那這個時候是否是就此就結束了?不,在源碼中發現不了線索,我還有一個神器,官方文檔
command + shift + 0
,搜索
forwardingTargetForSelector
,官方文檔解釋的清清楚楚明明白白。
If an object implements (or inherits) this method, and returns a non-nil (and non-self) result, that returned object is used as the new receiver object and the message dispatch resumes to that new object. (Obviously if you return self from this method, the code would just fall into an infinite loop.)3d
若是一個對象實現(或繼承)此方法,並返回一個非nil
(和非self
)結果,則該返回的對象將用做新的接收者對象,消息分派將繼續到該新對象。日誌
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
未實現的方法在 Person
實現,而後 forwardingTargetForSelector
重定向到 Person
裏,這樣也不會形成崩潰。
當咱們在快速轉發的 forwardingTargetForSelector
沒有進行處理或者重定向的對象也沒有處理,則會來到慢速轉發的 methodSignatureForSelector
。經過查看官方文檔,methodSignatureForSelector
還要搭配 forwardInvocation
方法一塊兒使用,具體的能夠自行去官方文檔查看。
methodSignatureForSelector
:返回 sel
的方法簽名,返回的簽名是根據方法的參數來封裝的。這個函數讓重載方有機會拋出一個函數的簽名,再由後面的 forwardInvocation
去執行。forwardInvocation
:能夠將 NSInvocation 屢次轉發到多個對象。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是否爲須要轉發的,若是是則手動生成方法簽名並返回。
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
返回了簽名,則什麼也不會發生,也不會崩潰。
慢速轉發和快速轉發比較相似,都是將A
類的某個方法,轉發到B
類的實現中去。不一樣的是,forwardInvocation
的轉發相對更加靈活,forwardingTargetForSelector
只能固定的轉發到一個對象,forwardInvocation
可讓咱們轉發到多個對象中去。
// 報出異常錯誤
- (void)doesNotRecognizeSelector:(SEL)sel {
_objc_fatal("-[%s %s]: unrecognized selector sent to instance %p",
object_getClassName(self), sel_getName(sel), self);
}
複製代碼
resolveInstanceMethod
返回 NO
,就會來到 forwardingTargetForSelector:
,獲取新的 target
做爲 receiver
從新執行 selector
,若是返回nil
或者返回的對象沒有處理,進入第二步。methodSignatureForSelector
獲取方法簽名後,判斷返回類型信息是否正確,再調用 forwardInvocation
執行 NSInvocation
對象,並將結果返回。若是對象沒有實現methodSignatureForSelector,進入第三步。doesNotRecognizeSelector:
拋出異常 unrecognized selector sent to instance %p
。