OC方法调用之objc_msgSend动态解析

944 阅读7分钟

在前文# OC方法调用之objc_msgSend慢速查找分析中我们只关注了慢速查找的流程,至于慢速查找也没找到的情况还没有分析

NEVER_INLINE

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    ......
    // 慢速查找流程,如果找到了就跳转到goto去执行
    
    
    // No implementation found. Try method resolver once.
    // 这里是慢速查找也没找到的情况,是我们今天分析的重点
    // behavior是参数传入的,值为0011,因为 LOOKUP_RESOLVER = 0010
    // 所以behavior & LOOKUP_RESOLVER结果为true
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
    
        // 这行代码是确保当前方法只执行一次
        behavior ^= LOOKUP_RESOLVER;
        
        // 这个方法是今天研究的入口位置
        // 现在behavior的值为0001
        return resolveMethod_locked(inst, sel, cls, behavior);
    }
    
    
done:
    // 慢速查找到方法的逻辑,进行缓存等等操作
}

先介绍几个枚举值

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

我们来看一下behavior的值是什么,他是作为参数被传入的,是被MethodTableLookup方法调起的

.macro MethodTableLookup

    SAVE_REGS MSGSEND

    // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // 这里写的明白第三个参数是 
    // LOOKUP_INITIALIZE | LOOKUP_RESOLVER = 0011 也就是3
    // receiver and selector already in x0 and x1
    mov x2, x16
    mov x3, #3 //这里给第三个参数赋值为3也说明了behavior = 0011
    bl _lookUpImpOrForward

    RESTORE_REGS MSGSEND

.endmacro

从这里我们知道了behavior的值为0011

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

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());
    
    // 获取resolveInstanceMethod方法的sel
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    // 这里检查是否实现了resolveInstanceMethod方法
    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
        
        // 因为NSObject.mm方法中有resolveInstanceMethod的默认实现
        // 所以不会执行到这里
        return;
    }

    // 开始执行resolveInstanceMethod方法
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);

    // 一些支线流程,无关大局,先不看😁
}

这个方法就是检查我们是不是实现了+resolveInstanceMethod方法,如果实现了就调用一次,这个时候可能动态解析成功了,再去执行一遍查询流程看能不能查到方法

lookUpImpOrForwardTryCache

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
        // lookUpImpOrForward方法我们前文详细分析过
        // 这是从当前类的慢速查找开始进行查找流程,因为当前类是第一次使用
        // 所以当前类就不会存在缓存cache,然后是父类缓存->父类慢速查找这样递归
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

    // 如果不是第一次使用该类那么先进行快速查找
    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
    if (slowpath(imp == NULL)) {
        // 如果缓存没查到,同样还是从该类的慢速查找开始,向父类递归查询
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

done:
    // 处理异常情况
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
        return nil;
    }
    
    // 如果从当前类缓存中查到了就返回
    return imp;
}

这两个方法是在动态解析完之后进行了一次常规查询流程,如果查询到了imp就返回。

我们前面分析了实例对象的动态解析流程,下面再看一下类方法的动态解析流程

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

    //检查是否实现了resolveClassMethod方法
    if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
        // Resolver not implemented.
        // 因为在NSObject.mm中实现了resolveClassMethod,系统做了兜底方案
        // 所以不会执行到这里
        return;
    }

    // 这里的代码就是获取到元类nonmeta
    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);
        }
    }

    // 获取到元类nonmeta之后向其发送一条resolveClassMethod消息
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
    
    // 这里同样无关紧要,先不看
}

这里的流程就是检查是否实现了resolveClassMethod方法,如果实现了就执行一次resolveClassMethod方法,下面就是常规查询一次,但是这里不同的是执行完类方法的动态解析之后又查询了一次实例方法,这是为什么呢???

da435341f46a446d91b7e338f0a835fc_tplv-k3u1fbpfcp-watermark.webp

我们看到实例方法的查询流程是当前类对象->父类对象->...->NSObject

image.png

类方法的查询流程是:

当前元类对象->父类元类对象->...->根元类对象->NSObject

image.png

最后查到了NSObject,可以认为NSObject是存储实例方法的地方,所以如果NSObject的分类实现了resolveClassMethod那么是需要通过实例对象的查找流程进行查找的。

验证

@interface JPerson : NSObject
- (void)instanceMethod;
+ (void)classMethod;
@end

声明了这两个方法但是都没有实现

@implementation JPerson

+(BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(instanceMethod)) {
        IMP imp = class_getMethodImplementation(self, @selector(instanceMethod1));
        Method method = class_getInstanceMethod(self, @selector(instanceMethod1));
        const char *type = method_getTypeEncoding(method);
        return class_addMethod(self, sel, imp, type);
    }
    return [super resolveInstanceMethod:sel];
}

+(BOOL)resolveClassMethod:(SEL)sel{
    if (sel == @selector(classMethod)) {
        IMP imp = class_getMethodImplementation(objc_getMetaClass("JPerson"), @selector(classMethod1));
        Method method = class_getClassMethod(objc_getMetaClass("JPerson"), @selector(classMethod1));
        const char *type = method_getTypeEncoding(method);
        return class_addMethod(objc_getMetaClass("JPerson"), sel, imp, type);
    }
    return [super resolveClassMethod:sel];
}


+(void)classMethod1{
    NSLog(@"classMethod1");
}

-(void)instanceMethod{
    NSLog(@"instanceMethod");
}

@end

instanceMethodimp动态解析为instanceMethod1

classMethodimp动态解析为classMethod1

image.png

执行结果符合预期,撒花🎉🎉🎉🎉🎉🎉

小问题

通过上面的分析我们引出一个小问题:类对象可以调用实例方法吗???

我们给NSObject创建一个分类

@interface NSObject (JZG)
-(void)aaaa;
@end



@implementation NSObject (JZG)
-(void)aaaa{
    NSLog(@"我是NSObject分类的实例方法");
}
@end

main方法中执行

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [JPerson performSelector:@selector(aaaa)];
    }
    return 0;
}

这里通过类对象JPerson来调用实例方法aaaa,执行结果为

image.png

我们看到执行成功了,在类和元类中存储的方法其实没有明确的标记他是实例方法还是类方法,我们只是根据其存储位置进行了区分,我们认为存储在类对象中的方法就是实例方法,存储在元类对象中的方法就是类方法,但是根元类对象是继承自NSObject,所以类对象是可以执行NSObject以及NSObject的分类中的方法的,最后再放一次那张熟悉的图,因为他真的太重要了

da435341f46a446d91b7e338f0a835fc_tplv-k3u1fbpfcp-watermark.webp

小结

我们调用一个方法其实是先通过objc_msgSend进行了快速查找慢速查找,如果查不到还可以通过动态解析弥补一次然后再进行方法的常规查询,但是同样还是可能查不到,那么后面我们还有消息转发流程等着我们去研究。

苹果设计动态解析有什么用处呢?

  • 苹果再给我们一次机会,先不报错
  • 监听所有未实现的方法
  • 我们以某一规范命名函数名称,在这里进行分类处理,例如push找不到目标页面可以跳转到一个备选页面
  • runtime动态派发,根据不同业务执行不同方法 但是也会引入一些问题:
  • 但是这样做也会使得业务流程变得更加复杂。
  • 因为动态派发只能在当前类(元类)或者父类(元类)中实现,甚至可以都放在NSObject的分类中来实现,但是无论放在哪一级都是自类对于父类的强依赖,增加代码耦合度

所以我们应该根据具体的需求进行合理使用,切不可设计过度适得其反。

参考文章

# objc_msgSend分析-动态解析+消息转发