动态方法决议与消息转发

上篇文章OC之消息发送的结尾,在lookUpImpOrForward函数中,我们提到,如果一个函数在cache、本类中、父类中都没有找到,那么就会调用resolveMethod_locked进行动态解析,本文就主要看下该过程。

动态方法决议

我们知道,在lookUpImpOrForward函数中,有如下代码:

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

记得我们在objc_msgSend未找到方法,然后调用lookUpImpOrForward的时候,在汇编下调用了MethodTableLookup方法,如下:

.macro MethodTableLookup
	
	SAVE_REGS MSGSEND

	// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
	// receiver and selector already in x0 and x1
	mov	x2, x16
	mov	x3, #3
	bl	_lookUpImpOrForward

	// IMP in x0
	mov	x17, x0

	RESTORE_REGS MSGSEND
复制代码

这里我们可以看到lookUpImpOrForward传入的第一个参数为实例对象obj,第二个为方法名sel,第三个类cls,第四个为LOOKUP_INITIALIZE | LOOKUP_RESOLVER1|2,也就是3

我们因此可以知道

  • behavior在初次进入的时候值为3;
  • LOOKUP_RESOLVER定义的该值为2;
  • behavior & LOOKUP_RESOLVER3 & 2值为2;
  • behavior ^= LOOKUP_RESOLVERbehavior = 3 ^ 2 = 1

然后进入到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()) {
        //cls类如果不是元类就走这里
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        //cls类如果是元类就走这里,然后调用类的决议方法
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            //如果cls类中的缓存和cls本类及继承链中都没有imp,则调用下方的方法
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
复制代码

我们知道cls->isMetaClass()的作用是判断cls是否是元类,并且对象的实例方法是存在类中的,而类方法是存在元类中的,因此这里:

  • 如果cls是类,也就是实例方法会调用resolveInstanceMethod方法,
  • 如果cls是元类,类方法则会调用resolveClassMethod方法,

这里的两个方法:resolveInstanceMethodresolveClassMethod。也称为方法的动态决议

实例方法动态决议

我们首先看下resolveInstanceMethod

resolveInstanceMethod

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    //首先定义resolveInstanceMethod的方法
    SEL resolve_sel = @selector(resolveInstanceMethod:);
    //先尝试在类的缓存中查找是否有该resolveInstanceMethod方法
    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
        //没有返回
        return;
    }
    //调用类的resolveInstanceMethod方法
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    //objc_msgSend(消息接收者,方法名,参数),相当于在类中调用resolveInstanceMethod方法,返回true代表处理了该方法,否则就有问题。
    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) {
        //处理错误信息
    }
}
复制代码

在这里resolveInstanceMethod函数的入参依次为,实例对象、方法名、类对象

  • 如果这个类之前缓存有resolveInstanceMethod方法,那么就直接调用然后返回了;
  • 如果这个类之前没有缓存,那么就需要这个类来调用resolveInstanceMethod方法。

不管哪一种,我们看到,cls类都是作为第一个参数来调用,我们可以知道resolveInstanceMethod方法其实是一个类方法。(实例方法在调用的时候,第一个参数为实例对象)也就是系统在找不到方法实现的时候,就会运行到这里,去类中找一个resolveInstanceMethod方法。我们可以验证下:


先创建一个FMEmployee的类,并只添加test1的方法声明,不做实现,并在类中添加resolveInstanceMethod的类方法,如下:

@interface FMEmployee : NSObject
- (void)test1;
@end

@implementation FMEmployee
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSLog(@"%s ---> %@",__func__,NSStringFromSelector(sel));
    return  [super resolveInstanceMethod:sel];
}
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Hello World!");
        FMEmployee *p = [[FMEmployee alloc] init];
        [p test1];
    }
}
复制代码

image.png 由于我们并没有做方法的实现,所以程序崩掉了,我们看到在崩溃前有打印test1方法,说明resolveInstanceMethod被调用了。

  • 那么该如何避免此崩溃呢
  • 另外在崩溃前,我们发现resolveInstanceMethod 函数被调用了两次,这又是为什么呢?

我们先看下如何避免崩溃,首先我们可以通过runtime来动态的添加方法的实现,如下图所所示

image.png 运行结果如下:

image.png 发现其可以正常运行。这时如果我们打印下FMEmployee的缓存,可以看到cache中缓存有test1函数,并且缓存的方法的实现为调用了resolveInstanceMethod函数进行方法决议后的method方法。 image.png

类方法动态决议

回到resolveMethod_locked方法中,我们看到如果是元类则会调用resolveClassMethod,我们先看下方法定义:

resolveClassMethod

/***********************************************************************
* resolveClassMethod
* Call +resolveClassMethod, looking for a method to be added to class cls.
* cls should be a metaclass.
* Does not check if the method already exists.
**********************************************************************/
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());
    // cls为元类,在元类中查找resolveClassMethod方法
    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) {
        。。。。
    }
}
复制代码

在这里我们可以先看下注释,说

  • 调用+resolveClassMethod
  • 查找要添加到类clsresolveClassMethod方法,
  • cls是一个元类

根据注释来看整一套流程其实也就是元类调用resolveClassMethod函数的过程。
既然如此,我们在FMEmployee中按照实例方法的流程添加一个类方法callFunc,但是不做实现,然后看看是否会调用resolveClassMethod,代码及实际运行如下:

@interface FMEmployee : NSObject
- (void)test1;
+ (void)callFunc;
@end

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

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Hello World!");
        [FMEmployee callFunc];
    }
}
复制代码

image.png

我们看到最终也是调用了resolveClassMethod方法,如果我们也通过运行时,把callFunc方法动态添加个方法实现,这时我们看到callFunc没有报错,且调用了method2的impimage.png

看完resolveClassMethod函数,我们再次回到resolveMethod_locked方法中,我们看到元类在调用了resolveClassMethod之后,如果元类中没有imp,那么又再一次调用了resolveInstanceMethod,这是为什么呢? image.png

  • 我们知道实例方法是存在类里边,而类方法是存放在元类中;
  • 当实例方法找不到的时候,就会进行实例方法决议,调用类中的resolveInstanceMethod方法;
  • 当类方法找不到的时候,我们调用类中的resolveClassMethod方法;但是如果没有resolveClassMethod方法,那么我们本应该调用元类的resolveInstanceMethod方法,但是元类我们是无法修改的,根据类与元类的继承关系,就会继续往根元类找,最终找到NSObjectresolveInstanceMethod方法。这一整套调用链路会变得非常长,影响系统运行效率;(如果NSObject没有resolveInstanceMethod方法,我们可以通过写分类进行添加)
  • 因此苹果提供resolveClassMethod方法,其实就是为了简化类方法的查找流程,方便在类方法找不到时,直接通过resolveClassMethod来进行类方法决议,提升调用效率;
  • resolveInstanceMethod其实才是获取方法决议的根本,如果提供的resolveClassMethod找不到,就需要再次调用resolveInstanceMethod

另: 在苹果的注释中有这么两行代码,

image.png 如果我们不调用resolveClassMethod改成[cls resolveInstanceMethod:sel];,并在NSObject的分类中添加上resolveInstanceMethod函数对callfunc函数的重定向,就会发现没有报错,并且走到NSObject分类中进行方法决议:

image.png

这也正是上文中所说的resolveInstanceMethod方法是决议根本。

resolveInstanceMethod 函数被调用了两次

resolveInstanceMethod 函数被调用了两次这又是怎么回事呢?对于这个现象可以分别打印一下这两次resolveInstanceMethod的堆栈信息:

image.png

  • 我们看到,第一次进入的时候堆栈信息显示,走的是方法的慢速查找,然后方法动态解析的流程,之后调用了resolveInstanceMethod方法;
  • 第二次进入的时候,先调用了___forwarding___,然后又调用methodSignatureForSelector等方法;这是是因为走了消息转发的流程,如果消息转发过程没有处理,又会调用class_getInstanceMethod函数,这个函数又会调用一次lookUpImpOrForward进行慢速查找,所以又会再调用一次resolveInstanceMethod;

image.png

总结

总结这部分动态方法决议的流程如下:

  • 如果没有找到目标方法就会调用resolveMethod_locked(inst, sel, cls, behavior)
  • 判断cls是否是元类
    • cls是元类
      • 调用resolveClassMethod(inst, sel, cls),使用类方法的动态方法决议
        • 去类中调用resolveClassMethod方法,如果没有,看继承链或者分类中是否有实现resolveClassMethod
      • lookUpImpOrNilTryCache(inst, sel, cls),类方法解析未找到或者为空
        • 如果没有找到或者为空,则执行resolveInstanceMethod(inst, sel, cls)
    • cls不是元类
      • 执行resolveInstanceMethod(inst, sel, cls),使用对象方法的动态方法决议,
        • 去类中调用resolveClassMethod方法,如果没有,看继承链或者分类中是否有实现resolveInstanceMethod
graph TB
meizhaodao[方法调用没找到]-->sNode[resolveMethod_locked] -->|不是元类|shili[resolveInstanceMethod]
shili -->|通过objc_msgSend|msgSendIns[调用类中的resolveInstanceMethod方法]-->other[缓存并调用具体实现的imp]
msgSendIns
shili .->|本类中没实现resolveInstanceMethod|object[看继承链或分类是否处理resolveInstanceMethod]

sNode -->|是元类|yuanlei[resolveClassMethod]
yuanlei-->|通过objc_msgSend|msgSendYL[调用类中的resolveClassMethod方法]-->other
yuanlei-->|元类中没实现resolveClassMethod|fenleiclass[看继承链或分类是否处理resolveClassMethod]
fenleiclass -->|也没处理 看缓存中是否有|huancun[lookUpImpOrNilTryCache].->object-->category[缓存并调用具体实现的imp或者崩溃]

消息转发

如果系统在动态决议阶段没有找到实现,就会进入消息转发阶段。在前边分析resolveInstanceMethod执行两次的时候,我们查看堆栈也看到,如果类中没有实现resolveInstanceMethod方法,就会调用methodSignatureForSelector方法,我们就看下消息转发流程。

日志打印

我们在看lookUpImpOrForward方法的时候,如果找到了方法,代码会跳转至done位置,然后调用log_and_fill_cache

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    省略代码...
 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);
}
复制代码

log_and_fill_cache方法中最主要的就是进行方法缓存cache.insert,但这里还有一个logMessageSend进行消息信息打印的过程,那么我们该如何获取这个打印的消息呢?

  • objcMsgLogEnabled我们看到if判断中有对打印控制的参数,说明可以通过设置这个参数来进行打印日志;全局查找,我们可以找到设置objcMsgLogEnabled的函数,也就是通过instrumentObjcMessageSends函数来设置是否打印日志。
  • /tmp/msgSends-%d通过查看logMessageSend的源码如下图可以看到,日志输出的路径为/tmp/msgSends-XXX,我们可以在此路径下找到这个日志打印文件。

image.png

image.png 我们调用instrumentObjcMessageSends函数前,需要先声明下该函数,如下:

extern void instrumentObjcMessageSends(BOOL flag);
复制代码

然后就可以通过以下方式打开该功能。

instrumentObjcMessageSends(YES);
FMEmployee *p = [[FMEmployee alloc] init];
[p test1];
instrumentObjcMessageSends(YES);
复制代码

如果有遇到lock 0x100803080 (runtimeLock) acquired before 0x100803000 (objcMsgLogLock) with no defined lock order这个问题,导致日志内容不打印,可以把下方代码注释掉 image.png 运行后,就可以在/tmp目录下找到该文件: image.png

image.png

可以看到这里在崩溃之前还调用了两个方法forwardingTargetForSelectormethodSignatureForSelector方法。消息发送在经过动态方法解析仍然没有查找到真正的方法实现,此时动态方法决议进入imp = forward_imp消息转发流程。转发流程分两步快速转发慢速转发

消息的快速转发

我们在FMEmployee类中声明test1方法,不去实现,然后定义一个FMBoy类在FMBoy类中实现test1,然后在FMEmployee类中实现forwardingTargetForSelector:方法,将FMEmployeetest1方法转发到FMBoy类中,也就是说,快速转发后的类必须有同名的方法。如下代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        FMEmployee *p = [[FMEmployee alloc] init];
        [p test1];
    }
}
----
@implementation FMEmployee
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
    if (aSelector == @selector(test1)) {
        return  [FMBoy new];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end
----
@interface FMBoy : NSObject
- (void)test1;
@end
@implementation FMBoy
- (void)test1{
    NSLog(@"%s",__func__);
}
@end
复制代码

然后运行 image.png 我们可以看到,虽然FMEmployee类没有定义test1方法,但是通过转发,FMBoy类的实例对象处理了该方法。
转发的作用在于,如果当前对象无法响应消息,就将它转发给能响应的对象。那么这时候方法缓存在哪?我们可以打印下:

image.png 我们发现方法缓存在接收转发消息的对象的cache中

消息的慢速转发

在快速转发过程中,如果我们不做处理,此时就会进入到methodSignatureForSelector方法, 也就是慢速转发。

@implementation FMEmployee

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"%s, aSelector = %@",__func__, NSStringFromSelector(aSelector));
    if (aSelector == @selector(test1)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return  [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
   NSLog(@"---%@---%@",anInvocation.target,NSStringFromSelector(anInvocation.selector));

}
@end
复制代码

这里 methodSignatureForSelector函数要跟forwardInvocation函数搭配使用,否则会报错。methodSignatureForSelector函数的作用是方法有效性签名,在提供了一个方法的有效签名之后,系统会去调用forwardInvocation方法来处理这个签名。

image.png 因此可以看到,test1函数虽然没有imp,但通过消息的转发,避免了崩溃。

消息转发流程图

graph TB
kaishi[对象接收到消息]-->fuleilian[首先从类 到 父类到继承链中查找方法]
fuleilian-->fineFunc[找到方法]-->cacheAndexecute[缓存并执行方法]
fuleilian-->nofineFunc[没有找到方法]
nofineFunc-->|进入动态方法决议流程|fangfajueyi[resolveInstanceMethod resolveClassMethod]
fangfajueyi-->addfangfajueyi[动态添加了方法]-->cacheAndexecute
fangfajueyi-->|没有动态添加方法|kuaisuzhuanfa[forwardingTargetForSelector快速转发]
kuaisuzhuanfa-->|转发消息|otherzhuanfa[其他对象执行方法]
kuaisuzhuanfa-->|不转发消息|manzhuanfa[methodSignatureForSelector慢速转发]
manzhuanfa-->|有效的选择器|forward[forwardInvocation]-->endNode[手动处理消息]
forward.->|不处理消息|throwerr[doesNotRecognizeSelector抛出异常]
manzhuanfa.->|没有方法签名 走forwarding|diaoyong[调用class_getInstanceMethod]-->ercijueyi[二次调用resolveInstanceMethod]
ercijueyi.->throwerr

从方法调用的角度看如何避免崩溃

方法决议阶段

如果一个函数没有实现imp,那么可以通过NSObject的分类,实现动态决议方法,来把没有具体实现imp的崩溃问题统一到分类中去处理,如下图所示:

image.png

快速转发阶段

如果一个函数没有实现imp,又没有做动态方法决议的相关处理,为了防止崩溃,也可以将这个消息转发给另一个对象去处理(另一个对象需有同名方法),如下图所示:

image.png

慢速转发阶段

那当一个函数没有实现imp,又没有做动态方法决议的相关处理,也没有进行消息的转发,那还可以通过慢速转发来处理该函数调用,如下图所示:

image.png 因此,动态决议、快速转发、慢速转发合称为三个救命稻草,用于防止方法查找导致的系统崩溃。

猜你喜欢

转载自juejin.im/post/7100003620115972126