七:底层探索 Runtime(2):消息转发

908 阅读5分钟

上一篇 文章中,我们了解了方法的调用,实际上就是发送消息,会经历一个快速查找,到慢速查找的流程,如果找到会直接返回,否则会进行动态方法解析,如果在动态方法解析这一步也没有做出处理,就会来到本章要介绍的内容 -- 消息转发

消息转发的入口

 imp = (IMP)_objc_msgForward_impcache;
 cache_fill(cls, sel, imp, inst);

一:消息转发快速流程

_objc_msgForward_impcache源码定义

STATIC_ENTRY __objc_msgForward_impcache
	// No stret specialization.
	b	__objc_msgForward
	END_ENTRY __objc_msgForward_impcache
	
	ENTRY __objc_msgForward
	adrp	x17, __objc_forward_handler@PAGE
	ldr	p17, [x17, __objc_forward_handler@PAGEOFF]
	TailCallFunctionPointer x17
	
	END_ENTRY __objc_msgForward

我们发现在__objc_msgForward_impcache函数中调用了__objc_msgForward函数。在__objc_msgForward中将__objc_forward_handler函数地址存入寄存器 x17 中,从x17 寄存器中将低 32 位的数据放到 p17 里 . 调用 x17 存储的函数 imp

_objc_forward_handler定义

__attribute__((noreturn)) void objc_defaultForwardHandler(id self, SEL sel)
{
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}

似乎似曾相识,其实之前有提到过,在这里系统会抛出异常信息。

难道这就结束了吗?显然没有!

2020-03-24 15:42:23.129475+0800 objc-test[10143:237241] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[WYStudent eat]: unrecognized selector sent to instance 0x100f02a90'
*** First throw call stack:
(
	0   CoreFoundation                      0x00007fff2caee8ab __exceptionPreprocess + 250
	1   libobjc.A.dylib                     0x000000010038c0aa objc_exception_throw + 42
	2   CoreFoundation                      0x00007fff2cb6db61 -[NSObject(NSObject) __retain_OA] + 0
	3   CoreFoundation                      0x00007fff2ca52adf ___forwarding___ + 1427
	4   CoreFoundation                      0x00007fff2ca524b8 _CF_forwarding_prep_0 + 120
	5   objc-test                           0x0000000100000e46 main + 70
	6   libdyld.dylib                       0x00007fff641167fd start + 1
	7   ???                                 0x0000000000000001 0x0 + 1
)

当崩溃发生的时候会有调用的堆栈,我们发现在崩溃前还调用了___forwarding___函数,但是它位于CoreFoundation框架中,具体怎么实现不得而知

其实在之前我们漏掉了一个小细节,在lookUpImpOrForward函数中当找到方法,会调用log_and_fill_cache进行缓存,其中有个log打印的开关objcMsgLogEnabled

log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (objcMsgLogEnabled) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cache_fill (cls, sel, imp, receiver);
}

logMessageSend中会输出所有 objc_msgSend()之后调用的函数,并保存在/tmp/msgSends-%d这个目录中

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

而开关objcMsgLogEnabled的赋值是根据在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;
}

我们将void instrumentObjcMessageSends(BOOL flag)导入到我们测试文件,进行调用来打印一些log信息

下面我们进行测试,eat方法只是声明,没有实现

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        instrumentObjcMessageSends(true);
        WYStudent *person = [WYStudent alloc];
        [person eat];
        instrumentObjcMessageSends(false);
        }
    return 0
    }

运行程序,程序崩溃后我们 找到 /tmp/msgSends-xxx 文件打开(以下只贴出该文件部分内容):

+ WYStudent NSObject initialize
+ WYStudent NSObject alloc
+ WYStudent NSObject resolveInstanceMethod:
+ WYStudent NSObject resolveInstanceMethod:
- WYStudent NSObject forwardingTargetForSelector:
- WYStudent NSObject forwardingTargetForSelector:
- WYStudent NSObject methodSignatureForSelector:
- WYStudent NSObject methodSignatureForSelector:
- WYStudent NSObject class
- WYStudent NSObject doesNotRecognizeSelector:
- WYStudent NSObject doesNotRecognizeSelector:
- WYStudent NSObject class

从打印的log来看,动态解析之后,还调用了forwardingTargetForSelector,它就是我们一直在找的突破口

查看关于forwardingTargetForSelector的官方文档:

  • 该方法的返回对象是执行sel的新对象,也就是自己处理不了会将消息转发给别人的对象进行相关方法的处理,但是不能返回self,否则会一直找不到
  • 该方法的效率较高,如果不实现或者nl,会走到forwardInvocation:方法进行处理
  • 底层会调用objc_msgSend(forwardingTarget, sel, ...);来实现消息的发送
  • 被转发消息的接受者参数和返回值等需要和原方法相同

因为我们可以实现forwardingTargetForSelector,将消息转发给其他对象来处理,实现消息的快速转发。

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

二:消息转发慢速流程

如果消息快速转发阶段未实现forwardingTargetForSelector方法,就会调用methodSignatureForSelector进行消息慢速转发,查看官方文档,其实就是返回一个方法签名,至于方法签名大家可以到这里看官方文档 TypeEncode

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

但是仅仅实现方法签名还不行,根据文档的说明,在实现方法签名的同时还必须实现- (void)forwardInvocation:(NSInvocation *)anInvocation;,两者是同时出现的。底层会通过方法签名,生成一个NSInvocation,将其作为参数传递调用。

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

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

三:总结

当方法查找流程结束后,如果仍然没有找到 IMP,runtime 首先进行 动态方法解析,之后再进入快速的消息转发,最后慢速消息转发:

    1. 动态方法解析:调用 +resolveInstanceMethod+resolveClassMethod 尝试获取 IMP
    1. 没有 IMP,进入快速消息转发,调用 -forwardingTargetForSelector:尝试获取一个可以处理的对象
    1. 仍没有处理,进入慢速转发,调用 -methodSignatureForSelector: 获取到方法签名后,将消息封装为一个invocation 再调用 -forwardInvocation:进行处理。 可见,当一个方法没有实现时,runtime 给了3次机会让我们进行处理。 下面是动态方法解析和消息转发的流程: