iOS 底層原理 - 消息轉(zhuǎn)發(fā)

在上一篇 iOS 底層原理 - 消息查找流程中,我們知道OC消息機(jī)制分為三個階段嘿棘,消息發(fā)送须误,動態(tài)解析和消息轉(zhuǎn)發(fā)挨稿,如果消息發(fā)送階段沒有找到方法,則會進(jìn)入動態(tài)解析階段京痢,負(fù)責(zé)動態(tài)的添加方法實現(xiàn)奶甘。我們先分析下動態(tài)解析階段。

動態(tài)方法決議階段

我們先來到_class_resolveMethod

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]

        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);  // 已經(jīng)處理
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
              // 對象方法 決議
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

首先會判斷cls是不是元類祭椰,如果cls不是元類的話臭家,說明調(diào)用的是實例方法,那就會調(diào)用_class_resolveInstanceMethod函數(shù)方淤,如果是元類的話钉赁,說明調(diào)用的是類方法,那么就會調(diào)用_class_resolveClassMethod函數(shù)携茂,并且調(diào)用完后會再次查找一下sel的指針你踩,找到了就會返回,如果還是找不到的話會調(diào)用_class_resolveInstanceMethod函數(shù)讳苦,這里可以用上面的isa走位圖解釋带膜。然后進(jìn)入_class_resolveInstanceMethod方法

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
//這里是一種容錯處理,判斷有沒有resolveInstanceMethod這個方法医吊,沒有就return钱慢,有就進(jìn)行下一步,如果一個類沒有繼承NSObject卿堂,是自己寫的resolveInstanceMethod這個方法束莫,這行就不會通過
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }
// 如果找到,則通過objc_msgSend調(diào)用一下+(BOOL)resolveInstanceMethod:(SEL)sel方法
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
// 再次尋找方法的IMP
    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

通過上面的源碼分析草描,我們知道览绿,如果要進(jìn)行動態(tài)解析的話,需要在方法resolveInstanceMethod里處理穗慕,給一個沒有實現(xiàn)的方法一個已經(jīng)實現(xiàn)的方法的IMP饿敲,就可以實現(xiàn)動態(tài)解析。
我們先調(diào)用一個對象方法逛绵,在這里我們調(diào)用一個不存在的saySomething方法怀各,給它sayHello方法的imp

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    
    if (sel == @selector(saySomething)) {
        NSLog(@"說話了");
        
        IMP sayHIMP = class_getMethodImplementation(self, @selector(sayHello));
    
        Method sayHMethod = class_getInstanceMethod(self, @selector(sayHello));
        
        const char *sayHType = method_getTypeEncoding(sayHMethod);
        
        return class_addMethod(self, sel, sayHIMP, sayHType);
    }
    
    NSLog(@"來了  老弟 - %p",sel);
    return [super resolveInstanceMethod:sel];
}

打印結(jié)果

2020-03-08 15:24:21.590396+0800 LGTest[7487:301370] 說話了
2020-03-08 15:24:21.590947+0800 LGTest[7487:301370] -[LGStudent sayHello]
Program ended with exit code: 0

然后看下類方法倔韭,需要實現(xiàn)_class_resolveClassMethod,這里要注意類方法在元類里面瓢对,需要把上面的self改為objc_getMetaClass("LGStudent")寿酌,這里劃重點。硕蛹。醇疼。

+ (BOOL)resolveClassMethod:(SEL)sel{
    
    NSLog(@"來了類方法:%s - %@",__func__,NSStringFromSelector(sel));

     if (sel == @selector(sayLove)) {
         NSLog(@"說- 說你你愛我");
         IMP sayHIMP = class_getMethodImplementation(objc_getMetaClass("LGStudent"), @selector(sayObjc));
         Method sayHMethod = class_getClassMethod(objc_getMetaClass("LGStudent"), @selector(sayObjc));
         const char *sayHType = method_getTypeEncoding(sayHMethod);
         // 類方法在元類 objc_getMetaClass("LGStudent")
         return class_addMethod(objc_getMetaClass("LGStudent"), sel, sayHIMP, sayHType);
     }
     return [super resolveClassMethod:sel];
}

打印結(jié)果為

2020-03-08 17:14:18.911042+0800 LGTest[8481:346767] 來了類方法:+[LGStudent resolveClassMethod:] - sayLove
2020-03-08 17:14:18.911631+0800 LGTest[8481:346767] 說- 說你你愛我
2020-03-08 17:14:18.911793+0800 LGTest[8481:346767] +[LGStudent sayObjc]
Program ended with exit code: 0

我們在驗證一下類方法找不到就會走_(dá)class_resolveInstanceMethod這個方法。
這里我們把上面的方法實現(xiàn)注釋掉法焰,然后在NSObject中添加一個對象方法sayLove秧荆,運(yùn)行下面代碼

+ (BOOL)resolveClassMethod:(SEL)sel{
    
    NSLog(@"來了類方法:%s - %@",__func__,NSStringFromSelector(sel));

//     if (sel == @selector(sayLove)) {
//         NSLog(@"說- 說你你愛我");
//         IMP sayHIMP = class_getMethodImplementation(objc_getMetaClass("LGStudent"), @selector(sayObjc));
//         Method sayHMethod = class_getClassMethod(objc_getMetaClass("LGStudent"), @selector(sayObjc));
//         const char *sayHType = method_getTypeEncoding(sayHMethod);
//         // 類方法在元類 objc_getMetaClass("LGStudent")
//         return class_addMethod(objc_getMetaClass("LGStudent"), sel, sayHIMP, sayHType);
//     }
     return [super resolveClassMethod:sel];
}

打印結(jié)果為

2020-03-08 17:13:15.772444+0800 LGTest[8446:345707] 對象方法-[NSObject(LG) sayLove]
Program ended with exit code: 0

消息轉(zhuǎn)發(fā)

如果沒有做動態(tài)解析,就會來到消息轉(zhuǎn)發(fā)階段埃仪。
我們先看一下找不到方法的崩潰信息乙濒,發(fā)現(xiàn)中間還經(jīng)過了forwarding和_CF_forwarding_prep_0,那我們可以猜想系統(tǒng)在消息轉(zhuǎn)發(fā)時可能做了其他的處理贵试。

*** First throw call stack:
(
    0   CoreFoundation                      0x00007fff447412fd __exceptionPreprocess + 256
    1   libobjc.A.dylib                     0x000000010035306a objc_exception_throw + 42
    2   CoreFoundation                      0x00007fff447bb056 __CFExceptionProem + 0
    3   CoreFoundation                      0x00007fff446e318f ___forwarding___ + 1485
    4   CoreFoundation                      0x00007fff446e2b38 _CF_forwarding_prep_0 + 120
    5   LGTest                              0x0000000100000d29 main + 89
    6   libdyld.dylib                       0x00007fff706043d5 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

下面開始驗證琉兜,我們在之前的方法消息查找流程中注意到填充緩存的時候我們會走log_and_fill_cache這個方法,是打印消息的這種形式毙玻。這里我們看到只有objcMsgLogEnabled為true的時候才會打印,那么繼續(xù)找它什么時候為ture廊散,

bool logMessageSend(bool isClassMethod,
                    const char *objectsClass,
                    const char *implementingClass,
                    SEL selector)
{
    char    buf[ 1024 ];

    // Create/open the log file
    if (objcMsgLogFD == (-1))
    {
        snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
        objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
        if (objcMsgLogFD < 0) {
            // no log file - disable logging
            objcMsgLogEnabled = false;
            objcMsgLogFD = -1;
            return true;
        }
    }

    // Make the log entry
    snprintf(buf, sizeof(buf), "%c %s %s %s\n",
            isClassMethod ? '+' : '-',
            objectsClass,
            implementingClass,
            sel_getName(selector));

    objcMsgLogLock.lock();
    write (objcMsgLogFD, buf, strlen(buf));
    objcMsgLogLock.unlock();

    // Tell caller to not cache the method
    return false;
}

通過instrumentObjcMessageSends這個方法來對objcMsgLogEnabled進(jìn)行賦值桑滩,也就是flag為true才會打印日志

void instrumentObjcMessageSends(BOOL flag)
{
    bool enable = flag;

    // Shortcut NOP
    if (objcMsgLogEnabled == enable)
        return;

    // If enabling, flush all method caches so we get some traces
    if (enable)
        _objc_flush_caches(Nil);

    // Sync our log file
    if (objcMsgLogFD != -1)
        fsync (objcMsgLogFD);

    objcMsgLogEnabled = enable;
}

那么我們通過在代碼中暴露instrumentObjcMessageSends方法,并定位在要崩潰的方法中允睹,可以打上日志运准,來查看調(diào)用

 instrumentObjcMessageSends(true);
 [student saySomething];
instrumentObjcMessageSends(false);

之后前往/tmp/msgSends 然后生成了文件 msgSends-,查看打印日志


屏幕快照 2020-03-14 下午3.32.09.png

發(fā)現(xiàn)經(jīng)過了resolveInstanceMethod缭受,forwardingTargetForSelector胁澳,methodSignatureForSelector,doesNotRecognizeSelector,這就是我們要尋找的處理方法米者。forwardingTargetForSelector韭畸,methodSignatureForSelector也就是我們下面要說的快速轉(zhuǎn)發(fā)流程和慢速轉(zhuǎn)發(fā)流程。

快速轉(zhuǎn)發(fā)流程 forwardingTargetForSelector

我們先找一下官方文檔蔓搞,我們可以得到它的返回參數(shù)是一個對象胰丁,如果這個對象非nil,非self的話喂分,系統(tǒng)會將運(yùn)行的消息轉(zhuǎn)發(fā)給這個對象執(zhí)行锦庸。否則,繼續(xù)查找其他流程蒲祈。意思就是一個無法識別的消息可以讓其他的對象來處理這個消息甘萧,系統(tǒng)給了個將這個sel轉(zhuǎn)給其他對象的機(jī)會萝嘁。


16f64591d863f64f.png

那么我們可以做以下處理,在LGTeacher中實現(xiàn)方法saySomething扬卷,然后交給LGTeacher去處理酿愧。

- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(saySomething)) {
        return [LGTeacher alloc];
    }
    return [super forwardingTargetForSelector:aSelector];
}

打印結(jié)果如下:

2020-03-14 15:37:30.290247+0800 008-方法查找-消息轉(zhuǎn)發(fā)[999:27953] -[LGStudent forwardingTargetForSelector:] -- saySomething
2020-03-14 15:37:30.291713+0800 008-方法查找-消息轉(zhuǎn)發(fā)[999:27953] -[LGTeacher saySomething]
Program ended with exit code: 0

慢速轉(zhuǎn)發(fā)流程

如果快速轉(zhuǎn)發(fā)階段沒有實現(xiàn),就會進(jìn)入到慢速轉(zhuǎn)發(fā)階段邀泉,也就是methodSignatureForSelector嬉挡。我們先找一下methodSignatureForSelector方法的文檔。


16f6471630fe64f2.png

這個方法會返回SEL方法的簽名汇恤,返回的簽名是根據(jù)方法的參數(shù)來封裝的庞钢,這個函數(shù)讓重載方有機(jī)會拋出一個函數(shù)的簽名,再由后面的forwardInvocation去執(zhí)行因谎。也就是forwardInvocation和methodSignatureForSelector必須是同時存在的基括。forwardInvocation這個函數(shù)可以將NSInvocation多次轉(zhuǎn)發(fā)到多個對象中,這也是這個方式靈活的地方财岔。forwardingTargetForSelector只能通過Selector 的形式轉(zhuǎn)向一個對象风皿。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(saySomething)) { // v @ :
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s ",__func__);
    
   SEL aSelector = [anInvocation selector];

   if ([[LGTeacher alloc] respondsToSelector:aSelector])
       [anInvocation invokeWithTarget:[LGTeacher alloc]];
   else
       [super forwardInvocation:anInvocation];
}

打印結(jié)果如下

2020-03-14 15:52:28.392549+0800 008-方法查找-消息轉(zhuǎn)發(fā)[1117:34015] -[LGStudent methodSignatureForSelector:] -- saySomething
2020-03-14 15:52:28.394561+0800 008-方法查找-消息轉(zhuǎn)發(fā)[1117:34015] -[LGStudent forwardInvocation:]
2020-03-14 15:52:28.395346+0800 008-方法查找-消息轉(zhuǎn)發(fā)[1117:34015] -[LGTeacher saySomething]
Program ended with exit code: 0

總結(jié)

1.objc_msgSend 從緩存中查找imp
2.慢速遞歸查找方法列表
3.沒有找到imp,那么看你有沒有進(jìn)行特殊處理匠璧,也就是消息動態(tài)解析桐款,如果沒有進(jìn)行特殊處理,就來到消息轉(zhuǎn)發(fā)階段夷恍。
4.快速消息轉(zhuǎn)發(fā) forwardingTargetForSelector魔眨,有沒有交給別人處理,如果沒有酿雪,就進(jìn)入慢速消息轉(zhuǎn)發(fā)
5.慢速消息轉(zhuǎn)發(fā)遏暴,意味著你不想處理,誰想要處理就去處理指黎。methodSignatureForSelector 實現(xiàn)方法簽名朋凉,forwardInvocation 來對消息處理
6.doesNotRecognizeSelector 系統(tǒng)不會分發(fā)這個事務(wù),報錯醋安。

最后附上流程圖


屏幕快照 2020-03-14 下午6.06.09.png

補(bǔ)充

探究的過程中杂彭,我們發(fā)現(xiàn)在動態(tài)方法決議階段,如果我們沒有處理動態(tài)方法決議茬故,會進(jìn)來兩次盖灸。

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    
    NSLog(@"來了老弟:%s - %@",__func__,NSStringFromSelector(sel));
    
    return [super resolveInstanceMethod:sel];
}

打印結(jié)果為

2020-03-14 18:11:50.658224+0800 LGTest[2037:82739] 來了老弟:+[LGStudent resolveInstanceMethod:] - saySomething
2020-03-14 18:11:50.659050+0800 LGTest[2037:82739] 來了老弟:+[LGStudent resolveInstanceMethod:] - saySomething
2020-03-14 18:11:50.659287+0800 LGTest[2037:82739] -[LGStudent saySomething]: unrecognized selector sent to instance 0x100f50650

這里為什么回來兩次呢,我們觀察一下控制臺打踊前拧:

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSLog(@"%s",__func__);
    return [super resolveInstanceMethod:sel];
}


- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    
    return [super forwardingTargetForSelector:aSelector];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(saySomething)) { // v @ :
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}
//
////
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s ",__func__);

       
}

結(jié)果如下:

2020-03-14 18:20:45.339901+0800 008-方法查找-消息轉(zhuǎn)發(fā)[2117:86023] +[LGStudent resolveInstanceMethod:]
2020-03-14 18:20:45.340428+0800 008-方法查找-消息轉(zhuǎn)發(fā)[2117:86023] -[LGStudent forwardingTargetForSelector:] -- saySomething
2020-03-14 18:20:45.340584+0800 008-方法查找-消息轉(zhuǎn)發(fā)[2117:86023] -[LGStudent methodSignatureForSelector:] -- saySomething
2020-03-14 18:20:45.340665+0800 008-方法查找-消息轉(zhuǎn)發(fā)[2117:86023] +[LGStudent resolveInstanceMethod:]
2020-03-14 18:20:45.340736+0800 008-方法查找-消息轉(zhuǎn)發(fā)[2117:86023] -[LGStudent forwardInvocation:]
Program ended with exit code: 0

從打印結(jié)果看出在methodSignatureForSelector和forwardInvocation之間還做了其他的操作赁炎,methodSignatureForSelector會返回一個方法簽名,之后會有一步去匹配簽名的過程,會調(diào)用class_getInstanceMethod方法徙垫。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末讥裤,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子姻报,更是在濱河造成了極大的恐慌己英,老刑警劉巖,帶你破解...
    沈念sama閱讀 223,002評論 6 519
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吴旋,死亡現(xiàn)場離奇詭異损肛,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)荣瑟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,357評論 3 400
  • 文/潘曉璐 我一進(jìn)店門治拿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人笆焰,你說我怎么就攤上這事劫谅。” “怎么了嚷掠?”我有些...
    開封第一講書人閱讀 169,787評論 0 365
  • 文/不壞的土叔 我叫張陵捏检,是天一觀的道長。 經(jīng)常有香客問我不皆,道長贯城,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,237評論 1 300
  • 正文 為了忘掉前任粟焊,我火速辦了婚禮冤狡,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘项棠。我一直安慰自己,他們只是感情好挎峦,可當(dāng)我...
    茶點故事閱讀 69,237評論 6 398
  • 文/花漫 我一把揭開白布香追。 她就那樣靜靜地躺著,像睡著了一般坦胶。 火紅的嫁衣襯著肌膚如雪透典。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,821評論 1 314
  • 那天顿苇,我揣著相機(jī)與錄音峭咒,去河邊找鬼。 笑死纪岁,一個胖子當(dāng)著我的面吹牛凑队,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播幔翰,決...
    沈念sama閱讀 41,236評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼漩氨,長吁一口氣:“原來是場噩夢啊……” “哼西壮!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起叫惊,我...
    開封第一講書人閱讀 40,196評論 0 277
  • 序言:老撾萬榮一對情侶失蹤款青,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后霍狰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體抡草,經(jīng)...
    沈念sama閱讀 46,716評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,794評論 3 343
  • 正文 我和宋清朗相戀三年蔗坯,在試婚紗的時候發(fā)現(xiàn)自己被綠了康震。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,928評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡步悠,死狀恐怖签杈,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情鼎兽,我是刑警寧澤答姥,帶...
    沈念sama閱讀 36,583評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站谚咬,受9級特大地震影響鹦付,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜择卦,卻給世界環(huán)境...
    茶點故事閱讀 42,264評論 3 336
  • 文/蒙蒙 一敲长、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧秉继,春花似錦祈噪、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,755評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至杠茬,卻和暖如春月褥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瓢喉。 一陣腳步聲響...
    開封第一講書人閱讀 33,869評論 1 274
  • 我被黑心中介騙來泰國打工宁赤, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人栓票。 一個月前我還...
    沈念sama閱讀 49,378評論 3 379
  • 正文 我出身青樓决左,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子哆窿,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,937評論 2 361