消息转发

422 阅读7分钟

经过上一篇 方法列表的查找(慢速发送流程) 发现我们的调用的方法是没有实现的话,runtime 还给了我们补救的机会,调用了 resolveMethod_locked 方法进行消息转发。接下来这篇内容就是对 消息转发 的探索。

我们新建一个类(Msg_Forward_Class),代码如下:

//Msg_Forward_Class.h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Msg_Forward_Class : NSObject

- (void)classInstanceMethod;
+ (void)classClassMethod;

- (void)abc;

@end

NS_ASSUME_NONNULL_END


//Msg_Forward_Class.m

#import "Msg_Forward_Class.h"

@implementation Msg_Forward_Class

//不实现 classInstanceMethod 方法

//不实现 classClassMethod 方法

@end

1、动态方法解析

我们通过源码看到了,如果调用的方法没有实现,那么根据当前 @selector(sel) 是类方法还是对象方法有2个分支,分别处理对象方法和类方法的消息转发。

oc 源码如下:

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();

    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        
        //对象方法的消息转发
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
       
       //类方法的消息转发
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    //可能调用解析器已经填充了缓存,所以尝试使用它
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

既然苹果分为两个分支处理,那我们也分两步探讨。

  • 未实现对象方法:[cls resolveInstanceMethod:sel]
  • 未实现类方法:[nonMetaClass resolveClassMethod:sel][cls resolveInstanceMethod:sel]

为什么未实现类方法还调用了 [cls resolveInstanceMethod:sel] 在稍后解析。

动态对象方法转发和类方法转发在主体结构上一样,流程图如下:

动态方法解析流程图

1、对象方法消息转发

那么我们点击 resolveInstanceMethod 查看方法实现

/***********************************************************************
* resolveInstanceMethod
* 调用+resolveInstanceMethod,寻找要添加到cls类的方法。

* cls可以是元类也可以是非元类。
* 不检查方法是否已经存在.
**********************************************************************/
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    
    //...
    
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
        // 解析器未实现。
        return;
    }

	//消息转发
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    //缓存结果(好或坏),这样解析器下次就不会触发。
    
    //这个方法是用于查询自己的有没有实现,没有查找父类,一旦找到就插入缓存,包括存入 _objc_msgForward_impcache
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
    
    //...
    
}

由上方代码中看到,消息转发的核心为下方3行代码:

//消息转发
SEL resolve_sel = @selector(resolveInstanceMethod:);

BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);

动态方法解析 寻找 resolveInstanceMethod 实现的过程,从当前类一直循环查找父类,直到 NSObject 的父类为 nil 结束。

2、类方法消息转发

去掉一些无用的代码,稍微修改了一下源码,发现,其实类消息转发和对象消息转发代码基本一样,只是对象消息转发是给从类中查找方法,而类消息转发是从元类中查找方法。

查找流程:元类->父元类->根元类->NSObject(元类)->NSObject(类)->nil,其实也就是根据 isa 的流程图进行查找。

resolveClassMethod 源码如下:

static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
 	
    //...
	
    SEL resolve_sel = @selector(resolveClassMethod:);

    if (!lookUpImpOrNilTryCache(inst, resolve_sel, cls)) {
        // Resolver not implemented.
        return;
    }

    Class nonmeta;
    {
        mutex_locker_t lock(runtimeLock);
        nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
        
        //...
    }
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(nonmeta, resolve_sel, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    //这个方法是用于查询自己的有没有实现,没有查找父类,一旦找到就插入缓存,包括存入 _objc_msgForward_impcache
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

    //...
}

但是在源码中看到,在类消息转发的分支里还调用了 resolveInstanceMethod(inst, sel, cls),这是为什么?

//类方法的消息转发
resolveClassMethod(inst, sel, cls);

//--------- 这里 --------
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
    resolveInstanceMethod(inst, sel, cls);
}

经过最上方动态方法解析的流程图我们基本上了解了 lookUpImpOrNilTryCache 的用途,是为了查找 @selector(sel) 有没有实现(找到就插入缓存)。

上述判断条件为:如果没有实现 @selector(sel) 方法,那么就会调用 resolveInstanceMethod。分析如下:

  • 1、操作和编程的只有自己创建的类文件,包括类方法也是写在类文件里,因为元类是编译时确定的,我们无法操作,所以断点会走到类的.m文件中;
  • 2、开发中不能直接操作 NSObject类 ,只能实现了 NSObjectcategory ,而工程中有的类基本上都由 NSObject 派生而来 ,所以,这也是苹果给下的一个的接口。
  • 3、根据isa 的流程图知道,所有的 meta class 最后都会指向 NSObject(root class), 因为万物之祖为 NSObject(root class)NSProxy除外!),所以对于 NSObject(root class) 看是否还调用了对象方法;

如果类消息转发最后也没有实现 [cls resolveInstanceMethod:sel] ,那动态方法解析流程就走完了。

2、快速消息转发

分析过动态方法解析流程之后,把上方代码简化成伪代码:

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    if (不是元类) {
        //对象方法的消息转发
    } else {
        //类方法的消息转发
        
        if (没有找到类方法消息转发的实现) {
           //对象方法的消息转发
        }
    }
    //可能调用解析器已经填充了缓存,所以尝试使用它
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

一步步断点,当最后 return lookUpImpOrForwardTryCache(inst, sel, cls, behavior); 后,就回到了 lookUpImpOrForward 方法中:

断点再走一步,我们看到调用了 jmp *%r11 ,这里的 imp = _objc_msgForward_impcache

继续断点下一步,看到 jmp __objc_msgForward 汇编 ,这里是调用了 __objc_msgForward ,可惜找不到 __objc_msgForward 的对应方法。

然后断点接着走,调用了

1 movq	__objc_forward_handler(%rip), %r11
2 jmp	*%r11

上方代码第一行就是在赋值 void *_objc_forward_handler = (void*)objc_defaultForwardHandler; 第二行跳转了 r11

看不到方法了,只能看汇编了,因为这个方法苹果没有开源。既然能断点,那就跟着断点去看看。

这里看到了2个重要的东西。一个是 __forwarding_prep_0______forwarding___,为什么这么说呢?是否记得有方法没有实现 crash 的时候 ,控制台打印的日志?是不是很眼熟?

到这里,重要的东西就来了。既然看到了控制台打印的信息了, 那再看看调用动态消息解析后发生了什么。

进入 call ___forwarding___,汇编如下:

这里看到了将 forwardingTargetForSelector: 方法指针赋值给了 r14 ,下方有 call %r14,截图截少了,抱歉。 call forwardingTargetForSelector:,这个就是 快速消息转发 系统调用。

3、完整消息转发

如果快速消息转发 forwardingTargetForSelector 没有实现的话,汇编继续往下走:

1、方法签名

上述汇编代码就看到了红色的方法注释 methodSignatureForSelector,在 NSObject.h 有方法原型 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

2、事务处理

有了方法签名,那还需要对完成方法签名的 SEL sel 判断是否处理,此时就需要用到 - (void)forwardInvocation:(NSInvocation *)invocation 方法了。可以选择不处理完成方法签名的 SEL sel,但是必须实现 forwardInvocation: 方法。

4、最后 crash 的地方

如果上述方法都没有完成对未实现方法调用的补救,那么就会 crash,从汇编中看,会调用下面这个方法:

NSObject.m 中实现如下:

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

上面这个太眼熟了对吧,crash 后控制台经常打印。

5、总结

到此,消息转发的流程就全部完成了。补一个控制台的 crash 的队栈信息:

看了上面的流程再看这个,是不是一目了然,是不是对这个 crash 信息再也不迷茫了!。

下方是消息转发的流程图:

5、关于 resolveInstanceMethod 调用2次的问题

在控制台上,发现动态消息转发打印了2次,其他都是一次。

现在把代码流程全部补全:

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

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

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSLog(@"%s  %@ 来了",__FUNCTION__, NSStringFromSelector(aSelector));
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    NSLog(@"%s  %@ 来了",__FUNCTION__, NSStringFromSelector(anInvocation.selector));
    return [super forwardInvocation:anInvocation];
}

再看控制台打印:

上面控制台第二次 resolveInstanceMethod 方法是在 methodSignatureForSelector 之后打印,看不到源码,所以开始汇编下手。

继续断点:

最后断点来到了这里,目前猜测是 methodSignatureForSelector 后,去匹配一下方法 imp 实现。

6、结尾

到此消息转发的内容就结束了,发消息这里分了3篇文章去写了。

下一篇:类的加载

PS:可以运行的并且不断进行注释的objc 源码地址