美文网首页
(2021 objc4-818源码分析)方法查找流程-动态决议&

(2021 objc4-818源码分析)方法查找流程-动态决议&

作者: 丸疯 | 来源:发表于2021-01-15 17:09 被阅读0次

方法查找流程

【第一步】 方法查找流程-快速查找流程
【第二步】 方法查找流程-慢速查找
【第三步】 动态决议
【第四步】 消息转发

动态决议

方法查找流程-慢速查找中,探索到如果通过慢速查找都没有找到,则会进行动态方法决议

    // No implementation found. Try method resolver once.

    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

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);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
动态方法决议图解.png
  • 如果没有找到就会调用resolveMethod_locked(inst, sel, cls, behavior)
  • 判断cls是否是元类
    • cls是元类
      • 执行resolveClassMethod(inst, sel, cls),使用类方法的动态方法决议
      • lookUpImpOrNilTryCache(inst, sel, cls),类方法解析是否找到或者为空
        • 如果没有找到或者为空,则执行resolveInstanceMethod(inst, sel, cls)
    • cls不是元类
      • 执行resolveInstanceMethod(inst, sel, cls),使用对象方法的动态方法决议

解决方法没有实现的思路一

通过上面的分析,我们知道如果没有找到方法的实现,会进行动态方法决议,那我们可以尝试针对类方法没有实现我们可以去实现resolveClassMethod,对象方法没有实现我们可以去实现resolveInstanceMethod

我们声明一个类,分别定义一个类方法和对象方法,不去实现它

+ (void)sayHello;
- (void)sayNB;
resolveClassMethod的实现

我们直接调用没有实现的类方法sayHello,运行发生崩溃,控制台打印结果如下:

+[Person sayHello]: unrecognized selector sent to class 0x100008208

解决方法:

+ (void)resolveClassMethodNotFound{
    NSLog(@"%s", __func__);
}

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

再次运行的结果

sayHello 来了
+[Person resolveClassMethodNotFound]

分析
通过上面的代码,确实在慢速查找没有找打方法的实现的时候,类方法会来到resolveClassMethod,此时,我们实现resolveClassMethod,就能防止崩溃,这里的做法是把当前的方法,动态的添加一个类方法,并将sayHello的实现转移给新添加的方法

resolveInstanceMethod的实现

我们直接调用sayNB对象方法,会报方法找不到的报错如下:

-[Person sayNB]: unrecognized selector sent to instance 0x100442da0

会提示实例方法,找不到。unrecognized selector sent to instance

解决方案:

- (void)resolveinstanceMethodNotFound{
    NSLog(@"%s", __func__);
}

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

这里没有找到sayNB的方法实现,来到resolveInstanceMethod,只需动态的添加一个方法,即可视做完成了sayNB的实现。这里的做法是获取resolveinstanceMethodNotFoundimp,和签名。绑定到sayNBsel,我们知道方法的存储是通过sel & _mask来获取得到存储下标,这样我们在调用sayNB的时候,默认调用的就是resolveinstanceMethodNotFound的实现。

修改之后的结果

 sayNB 来了
 -[Person resolveinstanceMethodNotFound]

resolveMethod_locked的实现中,我们看到如下代码

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

对于类方法,再执行了resolveClassMethod之后,会尝试lookUpImpOrNilTryCache,从缓存中去找,如果没有找到,也会执行resolveInstanceMethod实例方法的动态决议,是不是意味着我们不用实现resolveClassMethod只实现一个resolveInstanceMethod就行了。因为元类没有实体类,沿着继承链我们只能找到NSObject,所以我们给NSObject添加一分类,并实现resolveInstanceMethod,来验证一下:

处理方案

  • 在NSObject+SolveInstance.h中添加如下代码,
  • 因为NSObject是根类,super为nil,所有我们这里直接返回NO。
  • 我们只处理明确知道的sayNBsayHello两个没有实现的方法,避免产生不必要的崩溃
  • 不管是类方法,还是实例方法,找到NSObject都会变成实例方法,所以这里返回转移的方法实现定义为实例方法就行了
- (void)resolveinstanceMethodNotFound{
    NSLog(@"%s", __func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(sayHello) || sel == @selector(sayNB)) {
        NSLog(@"%@ 来了", NSStringFromSelector(sel));
        IMP imp = class_getMethodImplementation(self, @selector(resolveinstanceMethodNotFound));
        Method solveMethod  = class_getInstanceMethod(self, @selector(resolveinstanceMethodNotFound));
        const char *type = method_getTypeEncoding(solveMethod);
        return class_addMethod(self, sel, imp, type);
    }
    return NO;
}

跟我们预想的一样,打印结果如下

 sayHello 来了
 -[NSObject(SolveInstance) resolveinstanceMethod\M-LotFound]
 sayNB 来了
 -[NSObject(SolveInstance) resolveinstanceMethod\M-LotFound]

充分印证了:


isa与继承链.png

消息转发

打开消息发送的日志

我们跟log_and_fill_cache的源码的时候发现,消息的日志存储在/tmp/msgSends

消息发送日志存储.png
但是我们在这个目录下,并没有找到消息发送的日志,缺少了条件objcMsgLogFD == (-1),打开这个条件的方式如下:
extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        Person *person = [Person alloc];
        instrumentObjcMessageSends(YES);
        [person sayNB];
        instrumentObjcMessageSends(NO);
        NSLog(@"Hello, World!");
    }
    return 0;
}

这时我们就能拿到sayNB调用之后的消息日志了,不做任何崩溃处理,直接运行,在/tmp/msgSends目录下就获取到了消息日志

sayNB的消息调用日志.png
通过日志我们看到,再执行动态方法决议之后,还执行了forwardingTargetForSelectormethodSignatureForSelector
通过文档,我们还知道methodSignatureForSelector需要和forwardInvocation搭配起来使用。
接下来开始验证
消息转发-快速转发

forwardingTargetForSelector
在Person的.m文件中实现如下代码

实例方法-消息转发(快速转发)
/// 快速转发
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s- %@", __func__, NSStringFromSelector(aSelector));
    return [Student alloc];
}

// Student.m中的实现
- (void)sayNB {
    NSLog(@"%s", __func__);
}

打印结果如下

2021-01-18 15:38:14.107644+0800 消息转发流程[16585:199229] -[Person forwardingTargetForSelector:]- sayNB
2021-01-18 15:38:14.109524+0800 消息转发流程[16585:199229] -[Student sayNB]

实例方法可以转交给类方法么?代码如下

/// 快速转发
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s- %@", __func__, NSStringFromSelector(aSelector));
    return [Student class];
}

// Student.m中的实现
+ (void)sayNB {
    NSLog(@"%s", __func__);
}

打印如下:

2021-01-18 15:39:12.076271+0800 消息转发流程[16606:199968] -[Person forwardingTargetForSelector:]- sayNB
2021-01-18 15:39:12.077819+0800 消息转发流程[16606:199968] +[Student sayNB]

小结
对于实例方法,快速转发需要重写实例方法forwardingTargetForSelector

  • 如果转发的是一个,那么转发的类必须实现该消息的类方法实现

  • 如果是个对象,则需要实现该方法的实例方法实现
    如果以上不对应,或者没有实现,则会报错

  • 类方法-消息转发(快速转发)

/// 快速转发
+ (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s- %@", __func__, NSStringFromSelector(aSelector));
    return [Teacher class];
}

// Teacher.m的方法实现
+ (void)sayHello{
    NSLog(@"%s", __func__);
}

运行结果

2021-01-18 15:24:28.039137+0800 消息转发流程[16383:193004] +[Person forwardingTargetForSelector:]- sayHello
2021-01-18 15:24:28.040839+0800 消息转发流程[16383:193004] +[Teacher sayHello]

还可以有另外一种方式,代码如下

+ (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s- %@", __func__, NSStringFromSelector(aSelector));
    return [Teacher alloc];
}
// Teacher.m的方法实现
- (void)sayHello{
    NSLog(@"%s", __func__);
}

运行结果

2021-01-18 15:31:07.617114+0800 消息转发流程[16474:195831] +[Person forwardingTargetForSelector:]- sayHello
2021-01-18 15:31:07.619068+0800 消息转发流程[16474:195831] -[Teacher sayHello]

小结
对于类方法,快速转发需要重写类方法forwardingTargetForSelector

  • 如果转发的是一个,那么转发的类必须实现该消息的类方法实现
  • 如果是个对象,则需要实现该方法的实例方法实现
    如果以上不对应,或者没有实现,则会报错

快速转发总结

  • 类方法和实例方法有自己对应的forwardingTargetForSelector实现,需要重写对应的forwardingTargetForSelector方法
  • 当前类或对象处理不了,可以交给其它类或者对象来处理。
  • 如果转交的是,则需要实现同名类方法
  • 如果转交的是对象,则需要实现同名的实例方法
  • 如果其它类或对象没有处理或上面两个规则不对应,就会报错,所转发的类没有找打改方法的实现
消息转发-慢速转发

methodSignatureForSelector
forwardInvocation
在Person.m 文件中写下如下代码

/// 慢速转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s- %@", __func__, NSStringFromSelector(aSelector));
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s- %@", __func__, anInvocation);
}

运行,发现不报错,运行结果如下

2021-01-18 16:43:19.517884+0800 消息转发流程[17265:220490] -[Person methodSignatureForSelector:]- sayNB
2021-01-18 16:43:19.520205+0800 消息转发流程[17265:220490] -[Person forwardInvocation:]- <NSInvocation: 0x1004078a0>

forwardInvocation中加上如下代码

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s- %@", __func__, anInvocation);
    [anInvocation invokeWithTarget:[Student alloc]];
}

打印结果

2021-01-18 16:46:21.053838+0800 消息转发流程[17325:222827] -[Person methodSignatureForSelector:]- sayNB
2021-01-18 16:46:21.059838+0800 消息转发流程[17325:222827] -[Person forwardInvocation:]- <NSInvocation: 0x100606dd0>
2021-01-18 16:46:21.061812+0800 消息转发流程[17325:222827] -[Student sayNB]

同样anInvocationtarget可以是一个类,当需要实现同名的类方法
总结

  • methodSignatureForSelector需要搭配forwardInvocation使用,forwardInvocation系统会自动调用。
  • forwardInvocation可以不处理anInvocation事物,不会引发崩溃
  • anInvocationtarget可以是,也可以是实例对象,若指定了或者对象,必须实现对应的同名类方法或实例方法

我们将,动态方法决议,消息转发-快速转发,消息转发-慢速转发都放开,只在消息转发-慢速转发中处理,代码如下

/// 动态方法决议
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(sel));
    return [super resolveInstanceMethod:sel];
}

/// 快速转发
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    return [super forwardingTargetForSelector:aSelector];
}

/// 慢速转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s- %@", __func__, NSStringFromSelector(aSelector));
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s- %@", __func__, anInvocation);
    [anInvocation invokeWithTarget:[Student alloc]];
}

得出如下结果:

2021-01-18 17:27:22.544241+0800 消息转发流程[17843:242496] +[Person resolveInstanceMethod:] - sayNB
2021-01-18 17:27:22.546082+0800 消息转发流程[17843:242496] -[Person forwardingTargetForSelector:] - sayNB
2021-01-18 17:27:22.546903+0800 消息转发流程[17843:242496] -[Person methodSignatureForSelector:]- sayNB
2021-01-18 17:27:22.547638+0800 消息转发流程[17843:242496] +[Person resolveInstanceMethod:] - _forwardStackInvocation:
2021-01-18 17:27:22.549293+0800 消息转发流程[17843:242496] -[Person forwardInvocation:]- <NSInvocation: 0x100604450>
2021-01-18 17:27:22.549812+0800 消息转发流程[17843:242496] -[Student sayNB]

我们再去消息日志里面查看,得到的调用顺序同样是如上面所示。


消息转发图解.png
总结
  • 动态方法决议,在整个环节会被调用两次
    【第一次】在慢速查找没有查找的进入动态方法决议的时候是拯救查找的方法,
    【第二次】在慢速转发过程中调用是拯救_forwardStackInvocation:,可以直接在resolveInstanceMethod处理
  • 需要注意转发的是还是对象,做出对应的实现。

相关文章

网友评论

      本文标题:(2021 objc4-818源码分析)方法查找流程-动态决议&

      本文链接:https://www.haomeiwen.com/subject/fcviaktx.html