OC底层探究之动态方法决议

310 阅读7分钟

一、方法找不到的报错底层原理

如果一个方法只有声明没有实现,则会报出经典错误

unrecognized selector sent to instance

那么这个错误是怎么报出来的呢?

lookUpImpOrForward函数中,当父类为nil的时候,imp被赋值为forward_imp,而forward_imp_objc_msgForward_impcache

那么我们进入_objc_msgForward_impcache

	STATIC_ENTRY __objc_msgForward_impcache

	// No stret specialization.
	b	__objc_msgForward

	END_ENTRY __objc_msgForward_impcache

再进入__objc_msgForward

	ENTRY __objc_msgForward

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

TailCallFunctionPointer之前探索过,就是返回$0,即x17,而x17是在__objc_forward_handler

// Default forward handler halts the process.
__attribute__((noreturn, cold)) void
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

从这里也可以看出,底层根本就没有对象方法类方法的区分!只是判断self的类是不是元类!在元类则为类方法,不在元类则为对象方法

那是不是方法找不到就直接报错了呢?

并不是,方法找不到就会进入方法处理流程

二、对象方法的动态决议

1、特殊标志behavior

lookUpImpOrForward函数中没有找到方法的后会进入一个判断

// No implementation found. Try method resolver once.

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

behavior是之前快速查找时传入的:

.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

.endmacro

behavior3

搜索LOOKUP_RESOLVER

/* method lookup */
enum {
    LOOKUP_INITIALIZE = 1,
    LOOKUP_RESOLVER = 2,
    LOOKUP_NIL = 4,
    LOOKUP_NOCACHE = 8,
};

LOOKUP_RESOLVER2,所以此时的判断为:

behavior & LOOKUP_RESOLVER = 3 & 2 = 0x11 & 0x10 = 0x10 = 2

满足判断的条件!

接着:

behavior ^= LOOKUP_RESOLVER

=> behavior = behavior ^ LOOKUP_RESOLVER = 3 ^ 2 = 0x11 ^ 0x10 = 0x01 = 1

那么在不改变behavior的值的情况下,下次再进入到这个判断:

behavior & LOOKUP_RESOLVER = 1 & 2 = 0x01 & 0x10 = 0x00 = 0

即无法进入判断!

说明该判断一般情况只能进入一次!

2、resolveMethod_locked

接着进入到resolveMethod_locked函数:

/***********************************************************************
* resolveMethod_locked
* Call +resolveClassMethod or +resolveInstanceMethod.
*
* Called with the runtimeLock held to avoid pressure in the caller
* Tail calls into lookUpImpOrForward, also to avoid pressure in the callerb
**********************************************************************/
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);
}

先看returnlookUpImpOrForwardTryCache函数,根据名字可知是重新去缓存里面找方法:

IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{
    return _lookUpImpTryCache(inst, sel, cls, behavior);
}

再进入到_lookUpImpTryCache函数:

/***********************************************************************
* lookUpImpOrForward / lookUpImpOrForwardTryCache / lookUpImpOrNilTryCache
* The standard IMP lookup.
*
* The TryCache variant attempts a fast-path lookup in the IMP Cache.
* Most callers should use lookUpImpOrForwardTryCache with LOOKUP_INITIALIZE
*
* Without LOOKUP_INITIALIZE: tries to avoid +initialize (but sometimes fails)
* With    LOOKUP_NIL: returns nil on negative cache hits
*
* inst is an instance of cls or a subclass thereof, or nil if none is known.
*   If cls is an un-initialized metaclass then a non-nil inst is faster.
* May return _objc_msgForward_impcache. IMPs destined for external use 
*   must be converted to _objc_msgForward or _objc_msgForward_stret.
*   If you don't want forwarding at all, use LOOKUP_NIL.
**********************************************************************/
ALWAYS_INLINE
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;
}

这个方法先看到是否初始化,没有则慢速查找方法,有则先快速查找方法再慢速查找方法!

为什么之前明明已经查找过方法了,而且没有找到,这里还会继续查找呢?

因为在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);
        }
    }

如果是类不是元类则调用resolveInstanceMethod函数:

/***********************************************************************
* resolveInstanceMethod
* Call +resolveInstanceMethod, looking for a method to be added to class cls.
* cls may be a metaclass or a non-meta class.
* Does not check if the method already exists.
**********************************************************************/
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));
        }
    }
}

这里是给类发送消息,所以是去元类中找方法,即类方法!所以系统调用了resolveInstanceMethod方法!

3、动态添加方法

因为是给class发信息,所以是类方法

我们实现一下: image-20210713165721515

发现的确进入到了这个方法,而且进入了2次!

既然在方法报错前能够进入到这个方法,那么意味则可以在这个方法里面进行处理!比如动态添加方法!

先导入objc/runtime.h,然后添加方法:

image-20210713171605488

这就是对象方法的动态处理,这样就成功的避免了方法报错!

再回到resolveInstanceMethod方法,里面有个if判断:

    SEL resolve_sel = @selector(resolveInstanceMethod:);

    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
        return;
    }

那么这里是否会进入到return呢?

并不会!

先看看lookUpImpOrNilTryCache方法:

IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
{
    return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
}

这里调用的_lookUpImpTryCache方法,其实就是查找方法!

resolveInstanceMethod方法在NSObject中就已经实现了:

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;
}

所以这个判断是不会进入的!

接着就是调用resolveInstanceMethod方法,可以在里面进行一些处理,然后继续进行查找lookUpImpOrNilTryCache

因为已经添加了方法,所以再次进行查找的时候就可以找到方法并执行!

三、类方法的动态方法决议

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

如果是类方法的话,就会进入到else

先执行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());

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

和对象方法类似,先判断resolveClassMethod方法是否存在!

因为类方法是存在元类里面,所以在调用resolveClassMethod方法前,先调用getMaybeUnrealizedNonMetaClass方法通过元类获取当前类!

然后给类发消息,和对象方法的动态决议类似,也是类方法!

实现一下:

image-20210713175805713

这就是类方法的动态处理,这样就成功的避免了方法报错!

执行完resolveClassMethod方法后,还会通过判断如果依旧没有找到,则进入到resolveInstanceMethod方法。在resolveInstanceMethod方法中给元类发送消息,即在元类的元类中查找方法,所以是在根元类中查找方法!

既然对象方法和类方法都会进入到resolveInstanceMethod方法,那么是不是有一劳永逸的方式呢?

根据isa的走位图就可以得知,查找方法最终会到NSObject类中去查找!

所以在NSObject的分类中实现resolveInstanceMethod方法即可:

@implementation NSObject (KR)
- (void)sayOK{
    NSLog(@"%@ -- %s", self, __func__);
}

+ (void)sayYes{
    NSLog(@"%@ -- %s", self, __func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{

    //动态添加方法
    if (sel == @selector(sayNO)) {

        IMP sayYesIMP = class_getMethodImplementation(self, @selector(sayOK));
        Method method = class_getInstanceMethod(self, @selector(sayOK));
        const char* type = method_getTypeEncoding(method);

        return class_addMethod(self, sel, sayYesIMP, type);
    }else if (sel == @selector(sayHello)) {

        IMP sayYesIMP = class_getMethodImplementation(objc_getMetaClass("HPerson"), @selector(sayYes));
        Method method = class_getInstanceMethod(objc_getMetaClass("HPerson"), @selector(sayYes));
        const char* type = method_getTypeEncoding(method);

        return class_addMethod(objc_getMetaClass("HPerson"), sel, sayYesIMP, type);
    }

    NSLog(@"resolveInstanceMethod: %@ -- %@", self, NSStringFromSelector(sel));

    return NO;
}

@end

运行一下:

image-20210715171838916

成功避免了方法报错!