昨天遇到一个只有一行错误信息的问题:
-[NSNull objectForKey:]: unrecognized selector sent to instance 0x537e068
由于这个问题发生在次线程,所以没有太有用的堆栈信息,而是只有简单的SIGABRT信息:
考虑到unrecognized selector sent to instance这类问题是由于向某个对象发送了未实现的消息,这个过程大致如下(图片摘自这里):
参考Objective-C的对象模型:
struct objc_class { Class isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; const char *name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; struct objc_method_list **methodLists OBJC2_UNAVAILABLE; struct objc_cache *cache OBJC2_UNAVAILABLE; struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; #endif } OBJC2_UNAVAILABLE; /* Use `Class` instead of `struct objc_class *` */
消息发送的流程大致如下:
- 判断发送的消息是否为retain等内存管理方法;
- 判断receiver是否为nil;
- 判断是否在方法缓存中,即struct objc_cache *cache;
- 判断是否在方法列表中,即struct objc_method_list **methodLists,由于对象的方法可以动态添加,所以这里的类型是struct objc_method_list **,可以参考源文件;
- 判断是否在继承体系中——到这里,称之为Messaging过程。
- 如果实在找不到,就执行Dynamic Method Resolution过程,即尝试调用resolveInstanceMethod:或resolveClassMethod:方法,我们可以通过实现这两个方法来动态添加方法;
- 动态方法解析过程如果返回NO,那么还有最后的拯救机会,就是Message Forwarding消息转发过程(参考NSObject.h):
- (id)forwardingTargetForSelector:(SEL)aSelector; - (void)forwardInvocation:(NSInvocation *)anInvocation; - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
我第一反应是添加resolveInstanceMethod:来观察,这是一个类方法,所以得添加到metaClass上:
Class metaClass = objc_getMetaClass("NSNull"); SEL sel = @selector(resolveInstanceMethod:); const char *type = "c@::"; class_addMethod(metaClass, sel, (IMP)resolveInstanceMethod, type);
但遗憾的是,即便此时方法寻找不到时会调用到resolveInstanceMethod:方法,不过在设置的断点位置看也已经没有明确的堆栈信息了,所以我就直接添加找不到的方法objectForKey:来定位:
Class metaClass = objc_getMetaClass("NSNull"); SEL sel = @selector(objectForKey:); const char *type = "@@:@"; class_addMethod(metaClass, sel, (IMP)objectForKey, type);
这 样一来,通过在我们添加的objectForKey方法中设置断点就可以获取到详细堆栈信息,从而进一步定位到问题所在:
{ fromId = "\U6d4b\U8bd520#\U65fa\U4f01\U65e0\U7ebf\U6d4b\U8bd5"; msgContent = ""; msgSendTime = 1402909302; msgType = 12; uuid = 0; }
原因是由于服务端推送的消息中一个必填字段为空,而客户端也刚好在此处没有使用项目代码中约定的类型检查宏(此处应为VFDict),而是直接当做NSDictionary来操作。