消息轉(zhuǎn)發(fā)機(jī)制

iOS 項(xiàng)目中衰粹,我們經(jīng)常會遇到 x[xx xx]: unrecognized selector sent to instance xxxcrash粱锐,調(diào)用類沒有實(shí)現(xiàn)的方法就會出現(xiàn)這個經(jīng)典的 crash勺良,如下圖逝变,消息查找流程 這篇文章分析了如何找到報這個 crash 的原因嗤瞎,接下來我一步一步帶你分析原因以及如何避免此 crash欧引。

image.png

一、動態(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);

查看日志輸出如下:


image.png

然后通過在源碼中搜索 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é)的圖

image.png

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末则果,一起剝皮案震驚了整個濱河市幔翰,隨后出現(xiàn)的幾起案子漩氨,更是在濱河造成了極大的恐慌,老刑警劉巖遗增,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件叫惊,死亡現(xiàn)場離奇詭異,居然都是意外死亡做修,警方通過查閱死者的電腦和手機(jī)霍狰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來饰及,“玉大人蔗坯,你說我怎么就攤上這事×呛” “怎么了宾濒?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長瘫镇。 經(jīng)常有香客問我鼎兽,道長,這世上最難降的妖魔是什么铣除? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任谚咬,我火速辦了婚禮,結(jié)果婚禮上尚粘,老公的妹妹穿的比我還像新娘择卦。我一直安慰自己,他們只是感情好郎嫁,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布秉继。 她就那樣靜靜地躺著,像睡著了一般泽铛。 火紅的嫁衣襯著肌膚如雪尚辑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天盔腔,我揣著相機(jī)與錄音杠茬,去河邊找鬼。 笑死弛随,一個胖子當(dāng)著我的面吹牛瓢喉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播舀透,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼栓票,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了愕够?” 一聲冷哼從身側(cè)響起走贪,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤佛猛,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后厉斟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體挚躯,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年擦秽,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片漩勤。...
    茶點(diǎn)故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡感挥,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出越败,到底是詐尸還是另有隱情触幼,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布究飞,位于F島的核電站置谦,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏亿傅。R本人自食惡果不足惜媒峡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望葵擎。 院中可真熱鬧谅阿,春花似錦、人聲如沸酬滤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽盯串。三九已至氯檐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間体捏,已是汗流浹背冠摄。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留译打,地道東北人耗拓。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像奏司,于是被迫代替她去往敵國和親乔询。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評論 2 345