009:消息流程分析-动态方法决议&消息转发-[resolveInstanceMethod-forwardingTargetForSelector-methodSignatureForSelector-forwardInvocation]

问题

1:动态方法决议调用次数?

目录

1:消息发送的流程

2:动态方法决议

3: 消息转发

预备

 

正文

一:消息发送的流程

1:先去本类的缓存方法列表中查找,核心函数是CacheLookup

2:如果没有找到,就去本类的方法列表中查找,

3:如果当前方法列表还是没有,就通过 superClass指针在继承链中一直向上循环去查找,一直找到根 NSObject。在父类cache和方法列表中进行慢速查找。lookUpImpOrForward

4:如果查找过程中遇到父类==nil或者父类cache中的imp=imp_forward,没有找到,那么就进入 消息转发流程

5:到了消息转发流程还是没有处理的话,那么就会报 unrecognized selector错误。

二:动态方法决议

1:lookUpImpOrForward--消息转发值慢速查找

当一个方法没有实现时,也就是在 cache list和其继承关系的 method list中,没有找到对应的方法。这时会进入消息转发阶段,但是在进入消息转发阶段前,Runtime会给一次机会动态添加方法实现。

我们可以通过重写 resolveInstanceMethod和 resolveClassMethod方法,动态添加未实现的方法。

其中第一个是添加实例方法,第二个是添加类方法。这两个方法都有一个 BOOL返回值,返回 NO则进入消息转发流程

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
 上面代码及 循环体 省略...
/// 未找到实现。尝试方法解析器一次。
    // No implementation found. Try method resolver once.
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

2:resolveMethod_locked

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);
       ///如果 整个继承链 都没有找到 当前类方法的实现 还需要 调用一次 实例方法动态方法决议
       ///为什么  因为 元类的父类 是根源类(NSObject)
        ///     根元类的父类指向 类对象(NSObject) 实例方法存才类对象里 所以 需要调用一次 实例方法动态方法决议
    /// 此时为nil 说明 根元类 也没有找到 又因根源类super指向了 NSObject 类对象 所以需要进行 一次 类对象的动态方法决议
        if (!lookUpImpOrNil(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }
     // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}

这里就是我们需要研究的动态方法解析代码了。流程如下

  • 如果我们调用的是实例方法,那么 cls就不是元类,就会执行实例方法的动态决议。
  • 如果我们调用的是类方法,那么 cls就是元类,则会先调用类方法的动态解析。如果没有找到,我们还会调用实例方法的动态解析。这是调用元类的实例方法,根据继承链,会从根元类(元类的 isa会指向根元类)开始找,最终会找到 NSObject根类的 resolveInstanceMethod方法。
  • 注意:类方法存储在元类里面是实例方法

最后再调用lookUpImpOrForward重新查询一次。重新进行慢速查找,如果有动态添加sel对应的imp,则存入缓存中。

3:resolveInstanceMethod

针对实例方法调用,在快速-慢速查找均没有找到实例方法的实现时,我们有一次挽救的机会,即尝试一次动态方法决议,由于是实例方法,所以会走到resolveInstanceMethod方法,其源码如下

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);
    
    // look的是 resolveInstanceMethod --相当于是发送消息前的容错处理
    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
        // Resolver not implemented.
        return;
    }

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

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    //查找say666
    IMP imp = lookUpImpOrNil(inst, sel, cls);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

1:首先创建一个resolveInstanceMethod SEL

2:将此sel传进lookUpImpOrNil,又会进入lookUpImpOrForward慢速查找流程查找,在整个继承链的慢速查找看我们resolveInstanceMethod方法

  1、首先判断根类(NSObject)的子类存不存在 resolveInstanceMethod方法编号(sel)
  2、如子类 存在此方法编号 去 cache中通过 sel查询imp如果存在 返回imp
  3、如果子类只存在了方法编号(如空方法) 没有具体实现也就是imp不为真 再次进行 整个继承链的遍历向上查找
  4、肯定能找到 因为 NSObject 实现了 找到后并将其放入缓存

3:找到了 将其存入缓存 并返回imp没有找到 返回 forward_imp准备转发

4:resolveClassMethod

static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());

    //判断resolveClassMethod
    if (!lookUpImpOrNil(inst, @selector(resolveClassMethod:), cls)) {
        // Resolver not implemented.
        return;
    }

    Class nonmeta;
    {
        //对元类的简单处理
        mutex_locker_t lock(runtimeLock);
        nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
        // +initialize path should have realized nonmeta already
        if (!nonmeta->isRealized()) {
            _objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
                        nonmeta->nameForLogging(), nonmeta);
        }
    }
    //开始发送消息
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveClassMethod adds to self->ISA() a.k.a. cls
    //再次查找
    IMP imp = lookUpImpOrNil(inst, sel, cls);

    //判断出去
    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

5:代码示例

1:在LGPerson中重写resolveInstanceMethod类方法,将实例方法say666的实现指向sayMaster方法实现,如下所示

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(say666)) {
        NSLog(@"%@ 来了", NSStringFromSelector(sel));
        //获取sayMaster方法的imp
        IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
        //获取sayMaster的实例方法
        Method sayMethod  = class_getInstanceMethod(self, @selector(sayMaster));
        //获取sayMaster的丰富签名
        const char *type = method_getTypeEncoding(sayMethod);
        //将sel的实现指向sayMaster
        return class_addMethod(self, sel, imp, type);
    }
    
    return [super resolveInstanceMethod:sel];
}

2:在LGPerson类中重写resolveInstanceMethod方法,并将sayNB类方法的实现指向类方法lgClassMethod

+ (BOOL)resolveClassMethod:(SEL)sel{
    
    if (sel == @selector(sayNB)) {
        NSLog(@"%@ 来了", NSStringFromSelector(sel));
        
        IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        Method lgClassMethod  = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        const char *type = method_getTypeEncoding(lgClassMethod);
        return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
    }
    
    return [super resolveClassMethod:sel];
}

3:优化

上面的这种方式是单独在每个类中重写,有没有更好的,一劳永逸的方法呢?其实通过方法慢速查找流程可以发现其查找路径有两条

  • 实例方法:类 -- 父类 -- 根类 -- nil
  • 类方法:元类 -- 根元类 -- 根类 -- nil

它们的共同点是如果前面没找到,都会来到根类即NSObject中查找,所以我们是否可以将上述的两个方法统一整合在一起呢?答案是可以的,可以通过NSObject添加分类的方式来实现统一处理,而且由于类方法的查找,在其继承链,查找的也是实例方法,所以可以将实例方法 和 类方法的统一处理放在resolveInstanceMethod法中,如下所示

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(say666)) {
        NSLog(@"%@ 来了", NSStringFromSelector(sel));
        
        IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
        Method sayMethod  = class_getInstanceMethod(self, @selector(sayMaster));
        const char *type = method_getTypeEncoding(sayMethod);
        return class_addMethod(self, sel, imp, type);
    }else if (sel == @selector(sayNB)) {
        NSLog(@"%@ 来了", NSStringFromSelector(sel));
        
        IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        Method lgClassMethod  = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        const char *type = method_getTypeEncoding(lgClassMethod);
        return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
    }
    return NO;
}

这种方式的实现,正好与源码中针对类方法的处理逻辑是一致的,即完美阐述为什么调用了类方法动态方法决议,还要调用对象方法动态方法决议,其根本原因还是类方法在元类中的实例方法

三:消息转发

1:instrumentObjcMessageSends

1.1:在源码里知道了动态方法决议的信息 但是 转发我们再也找不到其相关的线索 所以下面我们使用下面方式来寻找其后续的流程

我们怎么找到它的 通过lookUpImpOrForward--> log_and_fill_cache--> logMessageSend,在其源码下面我们看到了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;
}

1.2:通过logMessageSend源码,了解到消息发送打印信息存储在/tmp/msgSends 目录,如下所示

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

1.3:开启instrumentObjcMessageSends

  1、不要在源码工程里面我们新建一个项目(否则崩溃日志为空)
  2、 在main中通过extern声明instrumentObjcMessageSends方法
  3、 打开 objcMsgLogEnabled开关,即调用instrumentObjcMessageSends方法时,传入YES
  4、运行并找到 /tmp/msgSends目录打开
///LGPerson.h
@interface LGPerson : NSObject
-(void)sayhello;
@end

///LGPerson.m
#import "LGPerson.h"
@implementation LGPerson
@end

/// main.m
extern instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        LGPerson * p =  [LGPerson alloc];

        instrumentObjcMessageSends(YES);
        [p sayhello];
        instrumentObjcMessageSends(NO);
 
        NSLog(@"Hello, World!");
    }
    return 0;
}

1.4:这个函数开启之后,会在 /private/tmp目录下创建一个 msgSends-xxxxxxxx是内部生成的一个编号

1:resolveInstanceMethod

2:forwardingTargetForSelector

3:methodSignatureForSelector

4:forwardInvocation

5:resolveInstanceMethod

6:doesNotRecognizeSelector

1.5:消息转发流程也可以分为两个阶段

  • 消息转发的处理主要分为两部分:

    • 【快速转发】当慢速查找,以及动态方法决议均没有找到实现时,进行消息转发,首先是进行快速消息转发,即走到forwardingTargetForSelector方法
      • 如果返回消息接收者,在消息接收者中还是没有找到,则进入另一个方法的查找流程

      • 如果返回nil,则进入慢速消息转发

    • 【慢速转发】执行到methodSignatureForSelector方法
      • 如果返回的方法签名nil,则直接崩溃报错

      • 如果返回的方法签名不为nil,走到forwardInvocation方法中,对invocation事务进行处理,如果不处理也不会报错

 

2:快速消息转发

我们可以在 forwardingTargetForSelector方法中将未实现的消息,转发给其他对象。可以在下面的示例代码中看到,返回响应未实现方法的其他对象。

@interface LGPerson : NSObject
- (void)sayHello;
@end

@implementation LGPerson
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%@",NSStringFromSelector(aSelector));
    return [LGStudent alloc];
}
@end

我们可以在 forwardingTargetForSelector方法中将未实现的消息,转发给其他对象。可以在下面的示例代码中看到,返回响应未实现方法的其他对象。

可以看到,消息接收者成功被转到 ZLPicker对象身上去了。但是如果 forwardingTargetForSelector方法未做出任何响应的话,就会来到 消息慢速转发流程上了。

3:慢速转发

3.1:方法签名对象

慢速消息转发时,首先会调用 methodSignatureForSelector方法,在方法内部生成 NSMethodSignature类型的方法签名对象。在生成签名对象时,可以指定 target和 SEL,可以将这两个参数换成其他参数,将消息转发给其他对象。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"来了老弟 %s", __FUNCTION__);
    if (aSelector == @selector(sayBad)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

生成 NSMethodSignature签名对象后,就会调用 forwardInvocation方法,这是消息转发中最后一步了。在这一步,只要我们重写了 forwardInvocation方法,就算不做任何操作,也不会发送消息找不到的崩溃了,只是这样会造成 事务的浪费。

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s - %@",__func__,anInvocation);
    anInvocation.target = [LGStudent alloc];
    [anInvocation invoke];
}

4:方法决议执行两次

 

5:总结 

如果一个实例方法不能在类和它的继承链的方法列表中不能被找到,则进入到方法解析和消息转发流程。

  • 1首先判断当前实例的类对象是否实现了 resolveInstanceMethod方法,如果实现了,会调用 resolveInstanceMethod方法。这个时候我们可以在 resolveInstanceMethod方法里动态的添加该 SEL对应的方法。之后会 重新执行查找方法实现的流程,如果依旧没找到方法实现,或者没有实现 resolveInstanceMethod方法,则进入消息转发流程。
  • 2调用 forwardingTargetForSelector方法,尝试找到一个能响应该消息的对象。如果找到了,则直接把消息转发给它,如果返回nil,则继续下一步流程。
  • 3调用 methodSignatureForSelector方法,尝试获得一个方法签名,如果获取不到,则直接调用 doesNotRecognizeSelector抛出异常信息。
  • 4调用 forwardInvocation方法,进行事务处理。如果不处理的话,则把事务抛出,爱谁谁接。

注意

 1:LGPerson中重写resolveInstanceMethod方法,并加上class_addMethod操作即赋值IMP,此时resolveInstanceMethod会走两次吗?

 

【 结论】:通过运行发现,如果赋值了IMP,动态方法决议只会走一次,说明不是在这里走第二次动态方法决议,

2:forwardingTargetForSelector

去掉resolveInstanceMethod方法中的赋值IMP,在LGPerson类中重写forwardingTargetForSelector方法,并指定返回值为[LGStudent alloc],重新运行,如果resolveInstanceMethod打印了两次,说明是在forwardingTargetForSelector方法之前执行了 动态方法决议,反之,在forwardingTargetForSelector方法之后

【结论】:发现resolveInstanceMethod中的打印还是只打印了一次,说明第二次动态方法决议 在forwardingTargetForSelector方法后

3:在LGPerson中重写 methodSignatureForSelector 和 forwardInvocation,运行

【结论】:第二次动态方法决议在 methodSignatureForSelector 和  forwardInvocation方法之间  

引用

 1:objc_msgSend 消息发送之 动态方法决议&消息转发

2:九、消息转发流程分析

3:iOS-底层原理 14:消息流程分析之 动态方法决议 & 消息转发

4:十、消息流程—动态方法决议 & 消息转发

5:OC底层原理十四:objc_msgSend(消息转发)

6:iOS 消息转发机制

7:objc_msgSend分析-动态解析+消息转发

8:IOS底层objc_msgSend&动态方法决议&消息转发

posted on 2020-12-01 17:48  风zk  阅读(652)  评论(0编辑  收藏  举报

导航