oc-底层原理之objc_msgSend慢速查找

548 阅读6分钟

在上一篇文章中(oc-底层原理之objc_msgSend方法快速查找)我们知道objc_msgSend在发送消息时首先会从缓存中去查找方法的IMP,但是如果缓存没有命中时会怎么处理呢?这一篇文章我们将关注objc_msgSend的慢速查找过程

lookUpImpOrForward

我们通过objc_msgSend源码知道,在缓存查找方法失败的时候会调用CheckMiss或者JumpMiss,我们看看这两个方法的源码:

.macro CheckMiss
	// miss if bucket->sel == 0
.if $0 == GETIMP
	cbz	p9, LGetImpMiss
.elseif $0 == NORMAL
	cbz	p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
	cbz	p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

.macro JumpMiss
.if $0 == GETIMP
	b	LGetImpMiss
.elseif $0 == NORMAL
	b	__objc_msgSend_uncached
.elseif $0 == LOOKUP
	b	__objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

因为传入的参数为NORMAL,所以两个方法最终都会调用__objc_msgSend_uncached,我们再来看看这部分源码:

STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
	
MethodTableLookup
TailCallFunctionPointer x17

END_ENTRY __objc_msgSend_uncached

我们看到会调用MethodTableLookup,继续源码:

.macro MethodTableLookup
	//部分代码已省略
	bl	_lookUpImpOrForward
	//部分代码已省略

.endmacro

lookUpImpOrForward是整个慢速查找的入口,接下来我们重点研究lookUpImpOrForward

lookUpImpOrForward源码分析

先上一份源码:

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
    if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.

    runtimeLock.lock();

    // We don't want people to be able to craft a binary blob that looks like
    // a class but really isn't one and do a CFI attack.
    //
    // To make these harder we want to make sure this is a class that was
    // either built into the binary or legitimately registered through
    // objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
    //
    // TODO: this check is quite costly during process startup.
    checkIsKnownClass(cls);

    if (slowpath(!cls->isRealized())) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
        // runtimeLock may have been dropped but is now locked again
    }

    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
        // runtimeLock may have been dropped but is now locked again

        // If sel == initialize, class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }

    runtimeLock.assertLocked();
    curClass = cls;

    // The code used to lookpu the class's cache again right after
    // we take the lock but for the vast majority of the cases
    // evidence shows this is a miss most of the time, hence a time loss.
    //
    // The only codepath calling into this without having performed some
    // kind of cache lookup is class_getInstanceMethod().

    for (unsigned attempts = unreasonableClassCount();;) {
        // curClass method list.
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            imp = meth->imp;
            goto done;
        }

        if (slowpath((curClass = curClass->superclass) == nil)) {
            // No implementation found, and method resolver didn't help.
            // Use forwarding.
            imp = forward_imp;
            break;
        }

        // Halt if there is a cycle in the superclass chain.
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache.
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            // Found a forward:: entry in a superclass.
            // Stop searching, but don't cache yet; call method
            // resolver for this class first.
            break;
        }
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }

    // No implementation found. Try method resolver once.

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

 done:
    log_and_fill_cache(cls, imp, sel, inst, curClass);
    runtimeLock.unlock();
 done_nolock:
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

其他的代码暂时可以先不看,重点关注for循环中的代码,for循环的大致执行流程如下:

  • curClass指向当前类,首先从当前类中查找,如果从当前类的methodList找到,则填充缓存并返回IMP

  • 如果当前类中并没有找到,当前类的父类不为空(至于父类为什么会为空,请去查看一下继承链),则curClass = curClass->superclass

  • curClass指向父类以后,首先通过cache_getImp从父类的缓存中查找,如果缓存中找到则直接返回IMP,否则,循环查找

  • 如果父类为nil,则imp会被赋值为forward_imp,forward_imp到底是什么呢 ?我们可以看到

    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    

    最终我们在汇编代码中找到了_objc_msgForward_impcache方法

    END_ENTRY __objc_msgForward_impcache
    
    	
    ENTRY __objc_msgForward
    
    adrp	x17, __objc_forward_handler@PAGE
    ldr	p17, [x17, __objc_forward_handler@PAGEOFF]
    TailCallFunctionPointer x17
    
    END_ENTRY __objc_msgForward
    

    最终调用的是__objc_forward_handler方法,我们来搜索一下(注意:搜索时请去掉第一个下划线),我们找到了_objc_forward_handler的实现:

    objc_defaultForwardHandler(id self, SEL sel)
    {
        _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                    "(no message forward handler is installed)", 
                    class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                    object_getClassName(self), sel_getName(sel), self);
    }
    void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
    

是不是很熟悉这段报错,我们都知道当方法未实现时,都会报错unrecognized selector sent to instance,这是因为当方法未找到时,都会返回一个IMP

如果没有找到对应的IMP,最终会执行:

// 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 (!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);
}

我们注意到最终还是会调用lookUpImpOrForward方法,不知你有没有疑问,如果IMP还是没有调用,是否会造成循环调用呢?

behavior异或

我们阅读源码发现,在调用resolveMethod_locked之前对behavior做了一个异或运算,这就保证了resolveMethod_locked在一次查找中只会调用一次,不会出现循环调用的情况

resolveMethod_locked

(源码请看上面,这里不再重复贴源码) 此处做了一个判断,当前的class是否是元类,最终会调用以下两个方法:

  • resolveInstanceMethod
  • resolveClassMethod

类方法在元类中以实例方法的形式存在,所以如果当前类是元类,则通过类方法的形式调用

resolveInstanceMethod

先来一段热腾腾的源码

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(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);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self 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 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));
        }
    }
}

这段源码比较简单,实际上就是

  • 判断类中resolveInstanceMethod是否实现,如果未实现,则直接返回
  • 如果实现了resolveInstanceMethod方法,则通过objc_msgSend发送消息

我们可以通过设置OBJC_PRINT_RESOLVED_METHODS环境变量来打印出resolveInstanceMethod的结果

我们通过一个demo来验证

首先定义一个WPerson

@interface WPerson : NSObject
- (void)sayNB;
- (void)sayHello;

@end

@implementation WPerson
- (void)sayNB{
    NSLog(@"%s",__func__);
}
@end

我们在类中只定义了两个方法sayNBsayHello,但是我们在m文件中只实现了sayNB方法,我们再通过WPerson对象调用sayHello方法

WPerson *person = [WPerson alloc];
[person sayHello];

我们都清楚这样调用会造成程序崩溃,那么我们在WPerson类中实现resolveInstanceMethod方法

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

打印结果:

[32756:432638] sayHello 来了
[32756]: RESOLVE: method -[WPerson sayHello] dynamically resolved to 0x100000c80
[32756:432638] -[WPerson sayNB]

resolveInstanceMethod方法中我们动态的将sayHello的实现映射到了sayNB方法实现上,所以最终调用sayNB方法

resolveClassMethod

实现原理同resolveInstanceMethod,当我们向类方法发送消息的时候,如果类方法的IMP未找到,就会调用这个方法,具体源码可自行探究,这里不再赘述

总结

oc的方法查找分为

  • 快速查找--从缓存中查找
  • 慢速查找--如果缓存中未找到,则从methodList查找(自己的methodList以及父类的methodList)
    • 动态方法决议:如果methodList中也没有找到对应的IMP,系统并不会马上抛出错误,而是通过查找resolveInstanceMethodresolveClassMethod来提供一个补偿的机会
    • 消息转发:(下一章节讨论)

通过以上的几种方式,共同组成了oc的方法查找流程