方法的动态决议和消息转发

上篇文章中我们分析了objc_msgSend的快速查找流程和慢速查找流程。在慢速查找流程中,我们知道objc_msgSend消息查找在lookUpImpOrForward方法中从父类递归去查找方法,直到找到imp。但是如果一直查找到父类为nil的时候还没有找到方法的实现,此时imp赋值为由_objc_msgForward_impcache的函数生成的forward_imp。那么今天我们就来分析一下这种条件下的方法查找处理逻辑。

方法的动态决议

1、resolveMethod_locked只执行一次的逻辑

首先我们还是先去objc源码里面去找一下_objc_msgForward_impcache函数的实现。从源码中我们发现_objc_msgForward_impcache函数也是通过汇编写的,其内部只调用了objc_msgForward的函数

iShot_2022-05-19_17.05.54.png

objc_msgForward的函数的函数内部有2行代码都是调用了objc_forward_handler的函数,最后调用了TailCallFunctionPointer函数,TailCallFunctionPointer函数在上篇文章中我们查看源码得知它其实仅仅只是一个函数指针的调用。那么我们去看一下objc_forward_handler函数里面做了什么样的操作

iShot_2022-05-19_17.14.45.png

我们看到objc_forward_handler = objc_defaultForwardHandler,而objc_defaultForwardHandler函数值做了方法找不到的报错反馈,其中用来区分类方法还是实例方法的条件仅仅是判断当前接收的对象是否是元类。又一方面证明了objc底层并没有去区分类方法和实例方法。了解了_objc_msgForward_impcache函数之后我们在lookUpImpOrForward方法中可以看到有以下几行代码

iShot_2022-05-19_17.22.53.png

从注释当中我们也可以知道,如果imp没有找到,他就会执行这几行代码去尝试解析方法一次。且只会执行一次,那么我们先看一下它是怎么实现只执行一次的逻辑的。首先在objc源码的main.m文件中添加以下调试代码

@interface MyClass : NSObject

- (void)method1;

@end

@implementation MyClass

@end
复制代码

iShot_2022-05-19_17.41.59.png

MyClass类里面声明一个没有实现的方法method1,运行objc源码当执行到[p method1]时在lookUpImpOrForward中也打个断点

iShot_2022-05-19_17.44.29.png

继续执行到lookUpImpOrForward方法中的断点处,也就是第一次执行进入lookUpImpOrForward方法时,此时behavior = 3,而LOOKUP_RESOLVER = 2

iShot_2022-05-20_01.45.57.png iShot_2022-05-20_01.46.47.png

那么if条件判断就等于3(11) & 2(10) = 2(10)为真,条件成立,此时会执行if内的代码,此时behavior ^= LOOKUP_RESOLVER(异或运算:相等为0,不等为1),也就是behavior = 3(11) ^ 2(10) = 1(01),并返回执行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);
}
复制代码

resolveMethod_locked函数内的操作来看,前面执行的是方法的动态决议,在动态决议之后最后会执行lookUpImpOrForwardTryCache函数,进入函数内部查看

IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{
    return _lookUpImpTryCache(inst, sel, cls, behavior);
}
复制代码
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertUnlocked();
    
    if (slowpath(!cls->isInitialized())) {
        // see comment in lookUpImpOrForward
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

    IMP imp = cache_getImp(cls, sel);
    if (imp != NULL) goto done;
#if CONFIG_USE_PREOPT_CACHES
    if (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) {
        imp = cache_getImp(cls->cache.preoptFallbackClass(), sel);
    }
#endif
    if (slowpath(imp == NULL)) {
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }
    
done:

    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
        return nil;
    }
    return imp;
}
复制代码

lookUpImpOrForwardTryCache函数会调用lookUpImpTryCache函数,在lookUpImpTryCache函数内我们发现在方法的动态决议之后,系统还会去cache里面找一遍方法,如果没有找到,即imp == NULL,此时又会再次执行lookUpImpOrForward方法,这时传入的behavior = 1,当第二次进入lookUpImpOrForward方法内断点处的if条件为1(01) & 2(10) = 0(00),条件不成立,不会执行if语句内的代码,所以它就实现只执行一次的逻辑。

2、动态决议

了解了这块代码只执行一次的逻辑之后,我们去分析一下resolveMethod_locked函数内方法的动态决议部分,在resolveMethod_locked方法内,我们看到系统会调用resolveInstanceMethodresolveClassMethod这两个方法。我们可以在类里面去重写resolveInstanceMethod和resolveClassMethod方法来为没有实现的方法通过runtime的API动态的添加方法的实现(为sel动态的添加imp)。那么我们分别去看一下这两个方法内部的实现

实例方法的动态决议resolveInstanceMethod
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    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.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNilTryCache(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));
        }
    }
}
复制代码

在方法内部我们看到就是objc_msgSend函数调用了resolveInstanceMethod这个类方法,接下来我们就在MyClass类里面去实现一下这个方法

@implementation MyClass

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

@end
复制代码

运行之后出现以下崩溃信息

iShot_2022-05-20_03.11.41.png

从崩溃信息中看到在method1方法没有实现的时候(找不到方法的IMP),在调用崩溃信息之前,就执行了resolveInstanceMethod方法,并且执行了两次。也就是说在方法找不到的实现(IMP)的时候,系统会调用resolveInstanceMethod方法。那么我们就可以在这个方法内通过runtime的API来动态的给这个sel添加一个imp

@implementation MyClass

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"%s--%@", __func__ , NSStringFromSelector(sel));
    if (sel == @selector(method1)) {
        IMP imp = class_getMethodImplementation(self.class, @selector(method2));
        class_addMethod(self.class, sel, imp, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

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

@end
复制代码

再次运行代码我们可以看到,当method1方法没有实现的时候,我们可以在resolveInstanceMethod方法里面为这个method1方法动态的添加一个method2的实现。在调用method1方法的时候系统就会去调用method2的实现。实例方法没有实现的时候我们就可以通过这种方法为这个sel添加一个imp

iShot_2022-05-20_03.28.26.png

同时在objc源码里resolveInstanceMethod方法内部在我们通过这个方法给sel添加一个imp之后,系统还会调用lookUpImpOrNilTryCache这个方法去cache里面找imp,那么此时系统能在cache里面找到这个方法的imp吗?也就是说我们为sel动态添加方法的实现之后,这个方法会被缓存到cache里面吗?我们去研究一下

iShot_2022-05-20_03.54.12.png

执行代码到断点处,为了防止cache的扩容机制导致method1的丢失,我们通过lldb调试命令为MyClasscache里面添加两个方法,让系统先进行cache扩容。(关于cache的相关内容可以去cache_t详解中了解)

iShot_2022-05-20_04.02.34.png

此时断点移到[p method1];下面后,去执行method1方法。这时候我们再去cache里面查找一下看有没有method1方法

iShot_2022-05-20_04.15.05.png iShot_2022-05-20_04.29.23.png

我们可以看到在caceh里面缓存了method1这个方法,也就是说通过resolveInstanceMethod方法动态的为sel添加imp的时候,同样会添加在cache缓存里

类方法的动态决议resolveClassMethod

如果是实例方法系统在resolveMethod_locked方法内会调用resolveInstanceMethod方法,如果是类方法,那么系统会调用resolveClassMethod这个方法进行方法的动态决议

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

    if (!lookUpImpOrNilTryCache(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 = lookUpImpOrNilTryCache(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));
        }
    }
}
复制代码

resolveInstanceMethod方法一样,在resolveClassMethod方法内部,系统一样通过objc_msgSend函数调用resolveClassMethod这个方法进行类方法的动态决议,之后调用lookUpImpOrNilTryCache方法从cache里面去找imp。同样,在MyClass类里面声明一个没有方法实现的类方法test,并实现resolveClassMethod方法

@interface MyClass : NSObject

- (void)method1;
+ (void)test;

@end

@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"%s--%@", __func__ , NSStringFromSelector(sel));
    if (sel == @selector(method1)) {
        IMP imp = class_getMethodImplementation(self.class, @selector(method2));
        class_addMethod(self.class, sel, imp, "v@:");
        return YES;
    }

    return [super resolveInstanceMethod:sel];
}

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

+ (BOOL)resolveClassMethod:(SEL)sel {
    NSLog(@"%s--%@", __func__ , NSStringFromSelector(sel));
    return [super resolveClassMethod:sel];
}

@end
复制代码

执行[MyClass test];之后,同样在打印崩溃信息之前系统也调用了2resolveClassMethod方法

iShot_2022-05-20_04.54.46.png

那么同理在resolveClassMethod方法内部我们也可以为类方法test动态添加方法的实现

+ (BOOL)resolveClassMethod:(SEL)sel {
    NSLog(@"%s--%@", __func__ , NSStringFromSelector(sel));
    if (sel == @selector(test)) {
        IMP imp = class_getMethodImplementation(self.class, @selector(method3));
        class_addMethod(objc_getMetaClass("MyClass"), sel, imp, "v@:");
        return YES;
    }
    return [super resolveClassMethod:sel];
}

- (void)method3 {
    NSLog(@"%s", __func__ );
}
复制代码

iShot_2022-05-20_04.59.20.png

那么如果我们对resolveClassMethodresolveInstanceMethod方法做以下调整之后

@implementation MyClass

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"%s--%@", __func__ , NSStringFromSelector(sel));

//    if (sel == @selector(method1)) {
        IMP imp = class_getMethodImplementation(self.class, @selector(method2));
        class_addMethod(self.class, sel, imp, "v@:");
        return YES;
//    }
    
//    return [super resolveInstanceMethod:sel];
}

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

+ (BOOL)resolveClassMethod:(SEL)sel {
    NSLog(@"%s--%@", __func__ , NSStringFromSelector(sel));
//    if (sel == @selector(test)) {
        IMP imp = class_getMethodImplementation(self.class, @selector(method4));
        class_addMethod(objc_getMetaClass("MyClass"), sel, imp, "v@:");
        return YES;
//    }

//    return [super resolveClassMethod:sel];
}

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

@end
复制代码

再执行[MyClass test];我们发现竟然最后调用了method2的方法实现

iShot_2022-05-20_05.07.38.png

出现这个现象的原因其实也好理解,因为在resolveClassMethod方法动态的添加方法实现的时候我们调用了class_getMethodImplementation这个方法去找method4的方法实现,传入的是类对象(self.class)。这个方法同样会进入resolveMethod_locked方法里面,此时在判断if (! cls->isMetaClass())(当前传入的cls是否不是元类)的时候条件为真,这时候系统就会走到resolveInstanceMethod这个方法里面去。所以最终调用的是method2的方法实现。如果把传入的类对象self.class改成传入元类对象objc_getMetaClass("MyClass"),系统在条件判断之后就会一直调用resolveClassMethod方法。此时就会出现死循环。

iShot_2022-05-20_05.10.00.png

resolveMethod_locked方法内,我们还看到系统在调用resolveClassMethod类方法的动态决议方法之后,系统会调用lookUpImpOrNilTryCache这个方法去cache里面找一遍方法的实现,如果在cache里面没有找到方法的实现。系统还会去调用resolveInstanceMethod这个实例方法的动态决议方法

`系统这样做的原因`
    通过继承链的关系我们知道类方法是存放在元类里面,而元类最终会继承自NSObject类,如果没有
    `resolveClassMethod`这个动态决议方法,系统在查找类方法的时候会走`resolveInstanceMethod`
    这个动态决议方法,这样的查找流程就会比较的长,会影响系统查找的效率。基于这个原因系统提供了
    `resolveClassMethod`方法来给类方法动态添加方法的实现,用来简化类方法的查找流程而提供给我们去实现
    的一个方法而已。系统其实最终都需要通过`resolveInstanceMethod`方法来进行方法的动态决议。
复制代码
AOP
  • 实例1

为NSObject添加一个分类FL,在分类里面实现resolveInstanceMethod方法,来解决方法找不到的崩溃问题(提高程序的稳定性)或对没有实现的方法的收集(crash收集)等

@interface NSObject (FL)

@end

@implementation NSObject (FL)

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"%s--%@", __func__ , NSStringFromSelector(sel));
    
//    if (sel == @selector(method1)) {
        IMP imp = class_getMethodImplementation(self.class, @selector(method2));
        class_addMethod(self.class, sel, imp, "v@:");
        return YES;
//    }

//    return [super resolveInstanceMethod:sel];
}

- (void)method2 {
    NSLog(@"%s", __func__ );
}
@end
复制代码
  • 实例2

通过runtime的API进行数据埋点。例如记录页面的停留时间,其中记录进入页面的时间核心代码如下

@implementation UIViewController (FL)

+(void)load
{
    static dispatch_once_t oncet;
    dispatch_once(&oncet, ^{
        Method method1 = class_getInstanceMethod(self.class, @selector(viewWillAppear:));
        Method method2 = class_getInstanceMethod(self.class, @selector(aopviewWillAppear));
        method_exchangeImplementations(method1, method2);
    });
}

-(void)aopviewWillAppear
{
    //在这里可以记录进入页面的时间
    NSLog(@"进来了   %@",self.class);
    //通过方法交换执行的是viewWillAppear,不会造成死循环
    [self aopviewWillAppear];
}

@end
复制代码

消息转发

lookUpImpOrForward方法中找到方法的imp之后会跳转到done执行log_and_fill_cache方法来进行方法的缓存

done:
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass();
        }
#endif
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
复制代码
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);
}
复制代码

在进行方法的缓存操作cls->cache.insert之前,还有一个条件判断objcMsgLogEnabled && implementer去执行logMessageSend方法向/tmp/msgSends文件里面写入信息

bool objcMsgLogEnabled = false;
static int objcMsgLogFD = -1;

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要为真,因为implementer是传入的类,它一定是存在且有值的。其中objcMsgLogEnabled默认是为false。我们在objc源码里去搜索一下objcMsgLogEnabled看哪些地方有给它赋值。搜索发现只有在instrumentObjcMessageSends方法里面才有可能会给objcMsgLogEnabled赋值为true

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;
}
复制代码

既然如此,那我们可以把这个函数用extern关键字导出出来,然后通过调用instrumentObjcMessageSends方法来主动向/tmp/msgSends下的文件内写入信息

iShot_2022-05-20_07.52.34.png

运行代码之后,当MyClass类里面没有实现test方法的时候,/tmp/msgSends下的文件内写入的信息如下

iShot_2022-05-20_07.58.21.png

从这个日志信息里面可以看出在调用resolveInstanceMethod动态决议方法之后和调用doesNotRecognizeSelector崩溃方法之前系统还调用了forwardingTargetForSelectormethodSignatureForSelector这两个方法,那么这两个方法执行的就是传说中的消息转发

1、消息的快速转发forwardingTargetForSelector

我们同样在MyClass类里面去重写这个方法

@interface MyClass : NSObject

-(void)test;

@end

@implementation MyClass

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%s", __func__ );
    return  nil;
}

@end
复制代码

当我们去调用MyClass类里面的test方法的时候,在崩溃之前系统的确调用了forwardingTargetForSelector方法

iShot_2022-05-20_08.27.22.png

我们在这个方法里面同样可以去做一些处理,当类不能响应某个方法的时候,可以在forwardingTargetForSelector方法里面去把这个消息转发给能够响应此方法的类,例如再创建一个ForwardingClass类,这个类里面实现了test方法

@interface ForwardingClass : NSObject

@end

@implementation ForwardingClass

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

@end
复制代码

然后在forwardingTargetForSelector方法里面去把test这个消息转发给ForwardingClass

@implementation MyClass

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%s", __func__ );
    if (aSelector == @selector(test)) {
        return  [ForwardingClass new];
    }
    return  nil;
}

@end
复制代码

此时再调用MyClass类的实例方法test就不会产生崩溃了。既然test已经转发给ForwardingClass类,那么此时test方法就会缓存在ForwardingClass类的cache里面。因为转发之后就不是MyClass类的实例对象在调用test方法,而是ForwardingClass类的实例对象在调用test方法。 iShot_2022-05-20_08.39.40.png

我们可以通过这个方法来把未处理的消息转发给一个单独的类,这个单独的类就可以去统一处理那些其他类里面没有处理的消息,也可以通过这个forwardingTargetForSelector方法去进行那些方法找不到的crash的收集等。这是对实例对象的方法的快速转发,对于类方法来说也有对应的消息转发的类方法+ (id)forwardingTargetForSelector:(SEL)aSelector,原理和- (id)forwardingTargetForSelector:(SEL)aSelector方法相同

2、消息的慢速转发methodSignatureForSelector

如果在消息的快速转发forwardingTargetForSelector方法里面也没有对消息做处理,那么系统就会调用消息的慢速转发methodSignatureForSelector方法。我们同样可以在MyClass类里面去重写methodSignatureForSelector方法。

@implementation MyClass

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

@end
复制代码

iShot_2022-05-20_09.02.15.png

需要注意的是methodSignatureForSelector方法需要和forwardInvocation方法一起使用,因为methodSignatureForSelector方法只是提供了一个方法的有效签名,提供了一个方法的有效签名之后,系统会去调用forwardInvocation方法来处理这个签名。例如在MyClass类里面实现forwardInvocation方法,此时再调用test就不会产生崩溃了。

@implementation MyClass

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

- (void)forwardInvocation:(NSInvocation *)anInvocation {

}

@end
复制代码

iShot_2022-05-20_09.33.07.png

同样我们也可以通过把这两个方法写在NSObject的分类里面来解决方法找不到的崩溃。也可以在forwardInvocation方法里面去对某个方法去做处理,在NSInvocation类型的参数anInvocation里面系统给我们提供了消息的target(消息接收者)和消息的selector(消息的方法名)。

iShot_2022-05-20_09.41.44.png

例如

- (void)forwardInvocation:(NSInvocation *)anInvocation {

    ForwardingClass * t = [ForwardingClass new];
    if ([self respondsToSelector:anInvocation.selector]) {
        //如果自己能够响应anInvocation中的selector,那么就自己响应这个方法
        [anInvocation invokeWithTarget:self];
    } else if ([t respondsToSelector:anInvocation.selector]) {
        //如果自己响应不了anInvocation中的selector,但是t可以响应,那么我们就把响应这个方法的对象变成t
        [anInvocation invokeWithTarget:t];
    } else {
        //都响应不了anInvocation中的selector
        NSLog(@"该功能正在开发中,敬请期待...");
    }
    
}
复制代码

消息的慢速转发(methodSignatureForSelectorforwardInvocation方法)、消息的快速转发(forwardingTargetForSelector方法)和方法的动态决议,就是系统给我们提供的防止崩溃的三根救命稻草。相比于方法的动态决议和消息的快速转发(forwardingTargetForSelector方法),消息的慢速转发(methodSignatureForSelectorforwardInvocation方法)更加灵活,我们甚至可以在forwardInvocation方法中不做任何操作,系统也不会发生因找不到方法的实现而产生的崩溃。结合上篇内容在OC底层对整个消息的发送流程如下图所示: 截屏2022-05-20 上午10.16.21.png

以上就是关于方法的动态决议和消息转发的原理

扩展内容

方法的动态决议走两次的原因

resolveInstanceMethod方法执行两次的原因其实也很简单,我们可以通过在resolveInstanceMethod加入断点,然后查看堆栈信息就可以知道。

第一次执行:

iShot_2022-05-20_10.43.15.png

第一次执行很好理解,结合上篇文章分析,在方法快速查找中查找不到的时候会执行objc_msgSend_uncached函数,在objc_msgSend_uncached中又会执行lookUpImpOrForward函数,然后进入resolveMethod_locked方法内,最后执行到resolveInstanceMethod这个方法的动态决议方法内。

第二次执行:

让代码继续向下执行,当第二次进入resolveInstanceMethod方法内时,我们可以通过bt指令打印一下此时的堆栈信息

iShot_2022-05-20_10.56.45.png

从堆栈信息中我们可以看到,当第一次执行结束之后会进入到_forwarding_这个函数内,在这个函数内会调用我们前面分析的消息的慢速转发的方法methodSignatureForSelector,系统在methodSignatureForSelector方法内会调用class_getInstanceMethod这个方法。我们去objc源码里面去找到这个方法的实现

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);
}
复制代码

我们可以看到在class_getInstanceMethod方法内有再一次调用了lookUpImpOrForward方法,在lookUpImpOrForward方法里面就会再一次调用resolveInstanceMethod方法。所以这就是resolveInstanceMethod方法被执行两次的原因。同样resolveClassMethod方法被执行两次也是这个原因。

猜你喜欢

转载自juejin.im/post/7099652640136495140