閱讀468 返回首頁    go 技術社區[雲棲]


利用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

  上一篇:go jQuery 省市區多級(三級/四級/五級。。。)聯動 BY 凨來了
  下一篇:go jvm開發筆記5 &#8211; 虛擬機內存管理