468
技術社區[雲棲]
利用objc的runtime來定位次線程中unrecognized selector sent to instance的問題
昨天遇到一個隻有一行錯誤信息的問題:
-[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 **,可以參考objc-class.m源文件;
- 判斷是否在繼承體係中——到這裏,稱之為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 = "<null>"; msgSendTime = 1402909302; msgType = 12; uuid = 0; }
原因是由於服務端推送的消息中一個必填字段為空,而客戶端也剛好在此處沒有使用項目代碼中約定的類型檢查宏(此處應為VFDict),而是直接當做NSDictionary來操作。
最後更新:2017-04-03 07:57:05