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、动态方法决议和消息转发流程总结

方法决议和消息转发

相关文章
相关标签/搜索