iOS探索底层-动态方法决议

639 阅读10分钟

前言

在之前的文章中,我们探索了方法的快速查找方法的慢速查找流程,那么如果在调用objc_msgSend的时候,经过快速查找和慢速查找依旧没有找到的话,系统接下来会怎么处理呢?我们在之前也稍微提到过一下,系统会给我们一次补救的机会,也就是动态方法决议流程,今天我们就来探索一下这个动态方法决议

动态方法决议

resolveMethod_locked方法

我们来回顾下上篇文章中的部分代码

    //**这个时候传入的behavior为LOOKUP_INITIALIZE|LOOKUP_RESOLVER**
    //**条件成立,会进入,然后重置behavior为LOOKUP_INITIALIZE**
    //**再次调用则不会进入**
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        //**动态方法决议流程**
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

当没有方法的慢速查找没有找到目标imp时,系统会调用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()) {
        //**不是元类,调用resolveInstanceMethod方法**
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
  
        //**是元类,调用resolveClassMethod**
        resolveClassMethod(inst, sel, cls);
        //**如果调用上面的方法还没有找到,尝试调用resolveInstanceMethod**
        //**原因是根据isa的继承链,根元类的父类是NSObject,所以在元类中如果没有找到**
        //**最后可能会在NSObjct中找到目标方法**
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }
    //**重新调用lookUpImpOrForwardTryCache方法,返回方法查找流程**
    //**因为已经进行过一次动态方法决议,下次将不会再进入,所以不会造成死循环**
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

很简单的一个方法,里面实际上就是判断我们传入的是否是元类,然后再调用不同的方法,我们先看看查找实例方法的流程,其中最重要的就是resolveInstanceMethod方法

resolveInstanceMethod方法

我们来看看resolveInstanceMethod的源码

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    //**如果目标类没有初始化,直接报错**
    ASSERT(cls->isRealized());
    //**创建一个方法名为resolveInstanceMethod的SEL**
    SEL resolve_sel = @selector(resolveInstanceMethod:);
    //**判断resolveInstanceMethod是否在目标类中实现**
    //**如果我们的类是继承自NSObject的话,那么这个判断就永远为false**
    //**因为在NSObject中已经默认实现了resolveInstanceMethod方法**
    //**因为是去cls->ISA也就是元类中查找,所以我们可以断定,resolveInstanceMethod是个类方法**
    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
        return;
    }
    //**强制类型转换objc_msgSend**
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    //**通过objc_msgSend方法调用resolveInstanceMethod**
    bool resolved = msg(cls, resolve_sel, sel);
    
    //**拼接上LOOKUP_NIL参数后,重新调用方法查找流程**
    //**虽然调用了resolveInstanceMethod,但是这个返回值是bool**
    //**所以我们要获取对应的imp,还是需要通过方法查找流程**
    //**如果通过resolveInstanceMethod添加了方法,就缓存在类中**
    //**没添加,则缓存forward_imp**
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
    //**组装相应的信息**
    if (resolved  &&  PrintResolving) {
        if (imp) {
            ………
        }
        else {
            ………
        }
    }
}

这个方法也相对比较简单

  • 首先创建一个方法名为resolveInstanceMethodSEL对象resolve_sel;
  • 然后判断resolve_sel是否实现,如果继承NSObject则必定已经实现,这里通过cls->ISA可以知道,resolveInstanceMethod是个类方法
  • 通过objc_ msgSend直接调用resolveInstanceMethod方法,因为是直接对cls发送消息,所以也可以看出resolveInstanceMethods类方法;
  • 调用lookUpImpOrNilTryCache方法,重新返回到方法查找的流程当中去;

resolveClassMethod方法

我们再来看看类方法的流程

static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());
    //**判断resolveClassMethod是否实现,NSObject中默认实现**
    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);
        }
    }
    //**强制类型转换objc_msgSend**
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    //**通过objc_msgSend调用类中的resolveClassMethod方法**
    bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);

    //**拼接上LOOKUP_NIL参数后,重新调用方法查找流程**
    //**类方法实际上就是元类对象中的对象方法,所以可以通过方法查找流程在元类中查找**
    //**如果通过resolveClassMethod添加了,就缓存方法在元类中**
    //**没添加,则缓存forward_imp**
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

    if (resolved  &&  PrintResolving) {
        if (imp) {
           ……
        }
        else {
           ……
        }
    }
}

这个方法与resolveInstanceMethod比较类似,如果通过resolveClassMethod方法添加了目标imp,则将其缓存在目标元类中,否则缓存forward_imp

lookUpImpOrNilTryCache方法

我们接下来看看,在resolveInstanceMethodresolveClassMethod中都会调用的lookUpImpOrNilTryCache方法

extern IMP lookUpImpOrNilTryCache(id obj, SEL, Class cls, int behavior = 0);

IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
{
    //**这里behavior没传,所以是默认值0**
    //**behavior | LOOKUP_NIL = 0 | 4 = 4**
    return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
}

非常简单的方法,就是给参数behavior拼接上LOOKUP_NIL然后调用_lookUpImpTryCache方法。

_lookUpImpTryCache方法

ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertUnlocked();
    //**判断类是否初始化**
    if (slowpath(!cls->isInitialized())) {
        //**没有初始化直接调用lookUpImpOrForward**
        //**里面针对没初始化的类,有相关处理**
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }
    //**去缓存中,查找sel是否有对应的imp**
    IMP imp = cache_getImp(cls, sel);
    //**找到了则跳去done**
    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
    //**没找到对应的imp,调用lookUpImpOrForward方法查找,behavior = 4**
    //** 4 & 2 = 0 ,所以这次方法查找不会再次进行动态方法决议**
    //**将_objc_msgForward_impcache缓存起来,方便下次直接返回**
    if (slowpath(imp == NULL)) {
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

done:
    //**命中缓存中,并且sel对应的imp为_objc_msgForward_impcache**
    //**说明动态方法决议已经执行过,且没有添加imp,则直接返回空**
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
        return nil;
    }
    //**说明动态方法决议中添加了对应的imp**
    return imp;
}

这个方法的流程

  • 首先判断是否初始化,如果没有初始化则直接调用lookUpImpOrForward,里面有针对没初始化的进行相应的处理;
  • 然后去缓存中进行方法的快速查找,找到了就去done
  • 缓存中没找到,如果支持共享缓存,则去共享缓存中查找
  • 都没有查找到,则通过慢速方法查找去查找方法,behavior = 4,这次慢速查找不会再次调用动态方法决议
  • done流程中,如果已经执行过动态方法决议且并没有添加imp,则缓存中的sel对应imp_objc_msgForward_impcache,这时直接返回nil。否则返回添加的imp实现。

到此,不管是还是元类动态方法决议流程就已经走完了,实际上就是系统在快速方法查找慢速方法查找都没有找到的情况下,给我们的一次补救的机会,最后还是会再次调用方法查找流程,并且再次调用的过程中不会再次调用动态方法决议流程,避免死循环

动态方法决议举例

首先请出我们的老朋友DMPerson

@interface DMPerson : NSObject
- (void)sayHello;
+ (void)sayBye;
@end

@implementation DMPerson

@end

实例方法举例

实现resolveInstanceMethod方法

DMPerson中实现resolveInstanceMethod方法

@implementation DMPerson
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    NSLog(@"--进入%@--",NSStringFromSelector(sel));
    return [super resolveInstanceMethod:sel];
}
@end

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

接着我们运行代码,查看结果

image.png 理所当然的,我们的程序崩溃了,但是我们在崩溃前,我们看到了我们在resolveInstanceMethod打印的东西,说明确实进入了我们的resolveInstanceMethod,那么我们自然就能够在其中进行一些奇奇怪怪的操作,来避免崩溃了。但是我们还发现resolveInstanceMethod调用了两次,这是为什么呢?我们来看看堆栈信息。

image.png 首先,当第一次进入resolveInstanceMethod时,我们看到确实是我们之前所说的流程,当快速方法查找慢速方法查找都没找到的时候,系统会给我们一次机会,进行动态方法决议

image.png 而第二次进入resolveInstanceMethod,我们发现并不是从main函数直接进入的,中间有个__forwarding__函数的调用,这个函数是系统在进行消息转发时底层调用的,所以我们暂且认为是系统在消息转发的过程中,再次调用了resolveInstanceMethod方法。具体的我们在消息转发的探索中,再进行说明。

动态添加sayHello

既然我们已经确定了当慢速方法查找没找到后,系统会给我们机会来弥补我们的错误,那么我们就要开始进行我们的补救操作了。

@implementation DMPerson
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    NSLog(@"--进入%@--",NSStringFromSelector(sel));
    if (sel == @selector(sayHello)) {
        IMP imp = class_getMethodImplementation(self, @selector(sayGood));
        Method method = class_getInstanceMethod(self, @selector(sayGood));
        const char *type = method_getTypeEncoding(method);
        //**动态给类添加`sayHello`的IMP为`sayGood`**
        return class_addMethod(self, sel, imp, type);
    }
    
    return [super resolveInstanceMethod:sel];
}

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

接下来运行代码咯

image.png 当我调用[DMPerson sayHello]的时候,并没有崩溃,而是打印输出了sayGood

类方法举例

实现resolveClassMethod方法

DMPerson中实现resolveClassMethod方法

@implementation DMPerson

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

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

同样运行代码

image.png 同样熟悉的味道,证明确实是方法未找到的情况下,进入了resolveClassMethod

动态添加sayNO

@implementation DMPerson

+ (BOOL)resolveClassMethod:(SEL)sel
{
    NSLog(@"--进入%@--",NSStringFromSelector(sel));
    if (sel == @selector(sayBye)) {
        IMP imp = class_getMethodImplementation(objc_getMetaClass("DMPerson"), @selector(sayNO));
        Method method = class_getInstanceMethod(objc_getMetaClass("DMPerson"), @selector(sayNO));
        const char *type = method_getTypeEncoding(method);

        return class_addMethod(objc_getMetaClass("DMPerson"), sel, imp, type);
    }
    
    return [super resolveClassMethod:sel];
}

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

由于类方法实际上是存储在元类中的,所以我们需要将方法添加到DMPerson元类中去,然后看看运行结果 image.png 成功的打印了sayNO,动态添加成功。

在NSObject中实现resolveInstanceMethod

我们成功的在实例方法类方法动态方法决议流程中,成功的动态添加了相应的IMP,让系统不会在没找到方法实现的情况下直接崩溃。但是现在我们有个疑问

我既然都能够在类里面动态决议了,这么费死劲的动态添加它,为什么我不直接实现这个方法呢?

接下来我们介绍动态方法决议的一种应用场景,我们对NSObject建个分类DMPerson+DM,然后在其中实现resloveInstanceMethod方法

@implementation NSObject (DM)
- (void)sayGood {
    NSLog(@"%s",__func__);
}

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

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(sayHello)) {
        IMP imp = class_getMethodImplementation(self, @selector(sayGood));
        Method method = class_getInstanceMethod(self, @selector(sayGood));
        const char *type = method_getTypeEncoding(method);

        return class_addMethod(self, sel, imp, type);
    }
    
    if (sel == @selector(sayBye)) {
        IMP imp = class_getMethodImplementation(objc_getMetaClass("DMPerson"), @selector(sayNO));
        Method method = class_getInstanceMethod(objc_getMetaClass("DMPerson"), @selector(sayNO));
        const char *type = method_getTypeEncoding(method);

        return class_addMethod(objc_getMetaClass("DMPerson"), sel, imp, type);
    }
    return NO;
}
@end

然后我们把之前DMPerson中的动态方法决议的代码都注释掉,然后在main中调用

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        DMPerson *p = [DMPerson alloc];
        [p sayHello];
        [DMPerson sayBye];
    }
    return 0;
}

看看打印结果 image.png 不管是类方法还是实例方法,我都成功的避免了他的崩溃。他的原因很简单

  • 如果是实例方法,那么根据isa的继承链最后会在NSObject中找到resolveInstanceMethod方法并调用,因此我们在NSObject中动态添加方法是可行的。
  • 如果是类方法,由于根元类的父类是NSObject的原因,所以还会调用一次resolveInstanceMethod,最后在NSObject中找到并执行。

那么通过这种方式,我们就能够将我们应用内所有的未找到方法导致的崩溃,全都避免了崩溃,并且能够收集起来,最后上传服务器,方便我们进行修复。

总结

整个方法的查找流程我们已经探索了大半,这是一个漫长而又复杂的过程。而系统把这个流程做的这么复杂的原因,就是为了保持系统的稳定性,所以给了我们一次机会去进行弥补。那么如果我们在动态方法决议流程中依旧没有弥补,接下来系统还会给机会吗?答案是,会的。那就是接下来我们要继续探索的消息转发流程,我们将在下篇文章中,继续进行探索。