OC底层探究之消息转发流程

666 阅读5分钟

一、消息转发流程引入

如果在动态方法决议的流程还是没有找到方法呢?最后会返回nil或者_objc_msgForward_impcache

那么是不是就没挽救的余地了呢?

我们可以通过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源码搜索instrumentObjcMessageSends

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);
}

再进入到logMessageSend

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文件夹,运行后就可以看到日志文件:

image-20210719153141911

我们发现resolveInstanceMethod是动态方法决议的过程,但是之后的forwardingTargetForSelector又是什么呢?

二、消息转发流程

1、快速转发流程

我们可以先command + shift + 0,来打开开发文档进行查阅:

image-20210719155834410

可以得知这个方法是一个重定向的过程!

在类中先重写forwardingTargetForSelector方法,因为我们调用的是对象方法,所以这里就是重写-方法并运行:

image-20210719160929802

发现确实进入到了forwardingTargetForSelector方法!

那么我们就可以把这个方法转交给其他类进行执行!

创建一个HClass类,实现sayNO方法:

image-20210719191523805

这样就完成了消息转发,并且不像动态方法决议那样臃肿!

以上就是快速转发流程了!

2、慢速转发流程

如果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];
}

三、反汇编消息转发流程

1、lldb查看堆栈

虽然消息转发流程以及有所了解了,但是我们并没有在源码中看到调用的过程,那么到底是怎么被调用的呢?

先用lldb中用bt查看堆栈:

image-20210722144456318

可以看到在方法报错前执行了3个方法:

__forwarding_prep_0___ --> ___forwarding___ --> doesNotRecognizeSelector

而且这3个方法都属于CoreFoundation动态库!

我们可以在苹果开源网站下载,但是在CoreFoundation动态库内并没有找到相对应的方法,说明苹果并没有完全的开源!

那么我们只能进行逆向了!

2、用Hopper逆向

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

用模拟器在lldb中用image list进行查看:

image-20210722151551119

然后把CoreFoundation拖入Hopper中,打开伪代码模式,搜索__forwarding_prep_0___函数:

image-20210722151711982

和我们在bt中看到流程一样,接着进入到了 ___forwarding___函数!

3、有forwardingTargetForSelector方法

进入 ___forwarding___函数:

image-20210722201752098

先判断是否有forwardingTargetForSelector方法。

如果forwardingTargetForSelector方法存在,即进入快速转发流程,调用forwardingTargetForSelector方法。

接着判断返回值:

image-20210722201856565

如果返回值为空或者和当前对象一样,则与没有forwardingTargetForSelector方法一样,进入到loc_115baf

如果有返回值或者和当前对象不一样,则经过处理后直接返回结果!

3、有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次!

五、动态方法决议和消息转发流程总结

方法决议和消息转发