OC底層探究之消息轉發流程

1、消息轉發流程引入

若是在動態方法決議的流程仍是沒有找到方法呢?最後會返回nil或者_objc_msgForward_impcachemarkdown

那麼是否是就沒挽救的餘地了呢?app

咱們能夠經過instrumentObjcMessageSends來打印objc在底層的相關日誌函數

@interface HPerson : NSObject
- (void)sayNO;
@end

@implementation HPerson

@end

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        HPerson * p = [HPerson alloc];
        
        instrumentObjcMessageSends(YES);
        [p sayNO];
        instrumentObjcMessageSends(NO);
    }
    return 0;
}
複製代碼

objc源碼搜索instrumentObjcMessageSendsoop

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;
}
複製代碼

這裏主要是給objcMsgLogEnabled賦值,而objcMsgLogEnabled則影響日誌打印:網站

/*********************************************************************** * log_and_fill_cache * Log this method call. If the logger permits it, fill the method cache. * cls is the method whose cache should be filled. * implementer is the class that owns the implementation in question. **********************************************************************/
static void log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer) {
#if SUPPORT_MESSAGE_LOGGING
    if (slowpath(objcMsgLogEnabled && implementer)) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cls->cache.insert(sel, imp, receiver);
}
複製代碼

再進入到logMessageSendui

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;
}
複製代碼

發現日誌寫入到了/tmp文件夾,運行後就能夠看到日誌文件:this

image-20210719153141911

咱們發現resolveInstanceMethod是動態方法決議的過程,可是以後的forwardingTargetForSelector又是什麼呢?url

2、消息轉發流程

一、快速轉發流程

咱們能夠先command + shift + 0,來打開開發文檔進行查閱:spa

image-20210719155834410

能夠得知這個方法是一個重定向的過程!3d

在類中先重寫forwardingTargetForSelector方法,由於咱們調用的是對象方法,因此這裏就是重寫-方法並運行:

image-20210719160929802

發現確實進入到了forwardingTargetForSelector方法!

那麼咱們就能夠把這個方法轉交給其餘類進行執行!

建立一個HClass類,實現sayNO方法:

image-20210719191523805

這樣就完成了消息轉發,而且不像動態方法決議那樣臃腫!

以上就是快速轉發流程了!

二、慢速轉發流程

若是HClass類並無實現sayNO方法呢?

那麼就會進入到methodSignatureForSelector方法,即慢速轉發流程!

依舊先打開開發文檔進行查閱:

image-20210719193415933

可得知這是一個返回方法簽名的過程!

在類中先重寫methodSignatureForSelector方法,由於咱們調用的是對象方法,因此這裏就是重寫-方法並運行:

image-20210719193016113

發現確實進入到了methodSignatureForSelector方法!

感受開發文檔可知這個方法須要搭配NSInvocation,以及返回適當的方法簽名,即NSMethodSignature

image-20210719195507480

成功執行,可是卻沒有任何的實現!

由於在iOS中有事務這個概念,便可執行也可不執行,所以方法保存到了簽名裏面,在有須要的時候便可提取:

image-20210719200305053

或者按照開發文檔的案例進行處理:

- (void)forwardInvocation:(NSInvocation *)invocation
{
    SEL aSelector = [invocation selector];
 
    if ([friend respondsToSelector:aSelector])
        [invocation invokeWithTarget:friend];
    else
        [super forwardInvocation:invocation];
}
複製代碼

3、反彙編消息轉發流程

一、lldb查看堆棧

雖然消息轉發流程以及有所瞭解了,可是咱們並無在源碼中看到調用的過程,那麼究竟是怎麼被調用的呢?

先用lldb中用bt查看堆棧:

image-20210722144456318

能夠看到在方法報錯前執行了3個方法:

__forwarding_prep_0___ --> ___forwarding___ --> doesNotRecognizeSelector

並且這3個方法都屬於CoreFoundation動態庫!

咱們能夠在蘋果開源網站下載,可是在CoreFoundation動態庫內並無找到相對應的方法,說明蘋果並無徹底的開源!

那麼咱們只能進行逆向了!

二、用Hopper逆向

要逆向首先要有可執行文件!

用模擬器在lldb中用image list進行查看:

image-20210722151551119

而後把CoreFoundation拖入Hopper中,打開僞代碼模式,搜索__forwarding_prep_0___函數:

image-20210722151711982

和咱們在bt中看到流程同樣,接着進入到了 ___forwarding___函數!

三、有forwardingTargetForSelector方法

進入 ___forwarding___函數:

image-20210722201752098

先判斷是否有forwardingTargetForSelector方法。

若是forwardingTargetForSelector方法存在,即進入快速轉發流程,調用forwardingTargetForSelector方法。

接着判斷返回值:

image-20210722201856565

若是返回值爲空或者和當前對象同樣,則與沒有forwardingTargetForSelector方法同樣,進入到loc_115baf

若是有返回值或者和當前對象不同,則通過處理後直接返回結果!

三、有methodSignatureForSelector方法

若是沒有forwardingTargetForSelector方法!

則進入到loc_115baf

iShot2021-07-23 12.00.35

1:判斷是不是殭屍對象,不是則繼續,是則跳轉到loc_115f34,即14處。

2:判斷是否有methodSignatureForSelector:方法,有則繼續,沒有則跳轉到loc_115f4a,即13處。

3:執行methodSignatureForSelector:方法,即慢速轉發流程,並判斷返回值,有值則繼續,值爲空則跳轉到loc_115fc5,即10處。

4:判斷是否有_forwardStackInvocation:方法,有則繼續,沒有則跳轉到loc_115d65,即7處。

5:執行_forwardStackInvocation:方法。

6:跳轉到loc_115ef5

7:沒有_forwardStackInvocation:方法,即跳轉到此,判斷是否有forwardInvocation:方法,有則繼續,沒有則跳轉到loc_115f92,即9處。

8:執行forwardInvocation:方法,並跳轉到loc_115dd2,和執行_forwardStackInvocation:方法同樣。

9:沒有forwardInvocation:方法,打印錯誤並繼續。

10 - 12:判斷是否有doesNotRecognizeSelector:方法,並執行,這就是最後的找不到方法的報錯

13:打印錯誤並跳轉到loc_115fbe,即10處。

14:打印錯誤並跳轉到loc_115f4a,即13

能夠看到不管是_forwardStackInvocation:方法仍是forwardInvocation:方法,最後都會到loc_115ef5

loc_115ef5:
    if (**___stack_chk_guard == **___stack_chk_guard) {
            rax = r15;
    }
    else {
            rax = __stack_chk_fail();
    }
    return rax;
複製代碼

即直接返回處理過的結果。

而全部的沒有找到相對應的方法最終都會執行doesNotRecognizeSelector方法:

// Replaced by CF (throws an NSException)
+ (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("+[%s %s]: unrecognized selector sent to instance %p", 
                class_getName(self), sel_getName(sel), self);
}

// Replaced by CF (throws an NSException)
- (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p", 
                object_getClassName(self), sel_getName(sel), self);
}
複製代碼

即方法沒有找到的報錯!

在反彙編流程中能夠看到系統調用了一個_forwardStackInvocation方法,這個方法並無對外暴露,可是咱們也能夠經過重寫這個方法來看看效果:

image-20210723110106640

確實和咱們看到的流程同樣,有_forwardStackInvocation方法的時候就不會在走forwardInvocation:方法了!

四、彙編流程總結

消息轉發彙編流程

4、動態方法決議被調用2次的緣由

以前在動態方法決議的時候,發現動態方法決議會被調用2次,這是爲何呢?

在objc源碼的對象動態方法決議裏面打上斷點,bt查看堆棧:

image-20210723151050708

發現第二進來是由於在CoreFoundation庫中的methodSignatureForSelector方法裏的__methodDescriptionForSelector方法調用了objc庫中的class_getInstanceMethod方法!

咱們先看看objc源碼中的methodSignatureForSelector方法:

 // Replaced by CF (returns an NSMethodSignature)
 + (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
     _objc_fatal("+[NSObject methodSignatureForSelector:] "
                 "not available without CoreFoundation");
 }
 ​
 // Replaced by CF (returns an NSMethodSignature)
 - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
     _objc_fatal("-[NSObject methodSignatureForSelector:] "
                 "not available without CoreFoundation");
 }
複製代碼

能夠發現是沒有CoreFoundation庫不可用!

說明真正的源碼是在CoreFoundation庫中,去反彙編中進行查看:

image-20210723151659159

發現確實有__methodDescriptionForSelector方法,進一步跟進:

image-20210723151956042

發現確實調用了class_getInstanceMethod方法,再去objc源碼中進行查看:

 /***********************************************************************  * class_getInstanceMethod. Return the instance method for the  * specified class and selector.  **********************************************************************/
 Method class_getInstanceMethod(Class cls, SEL sel)  {
     if (!cls  ||  !sel) return nil;
 ​
     // This deliberately avoids +initialize because it historically did so.
 ​
     // This implementation is a bit weird because it's the only place that 
     // wants a Method instead of an IMP.
 ​
 #warning fixme build and search caches
         
     // Search method lists, try method resolver, etc.
     lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);
 ​
 #warning fixme build and search caches
 ​
     return _class_getMethod(cls, sel);
 }
複製代碼

發現的確調用了lookUpImpOrForward函數!

因此動態方法決議會被調用2次!

5、動態方法決議和消息轉發流程總結

方法決議和消息轉發

相關文章
相關標籤/搜索