手把手带你探索Runtime底层原理(二)动态方法解析和消息转发

772 阅读8分钟

前言

继续上篇Runtime底层原理(一)方法查找,在上篇说到如果没有找到imp,就会结束方法查找,然后进入动态方法解析和消息转发。

动态方法解析

 // No implementation found. Try method resolver once.

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }
  • 这里判断了类有没有解析和是否尝试解析过的标记值triedResolver,再次解析后会把triedResolver设置为YES,只解析一次。
  • 重点在_class_resolveMethod这个解析函数
/***********************************************************************
* _class_resolveMethod
* Call +resolveClassMethod or +resolveInstanceMethod.
* Returns nothing; any result would be potentially out-of-date already.
* Does not check if the method already exists.
**********************************************************************/
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}
  • 判断了这个类是不是元类isMetaClass,如果是非元类则只调用_class_resolveInstanceMethod,如果是元类则两者都可能调用
    • 这里有个疑问,为什么要这么判断?

_class_resolveClassMethod分析

static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    assert(cls->isMetaClass());

    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                        SEL_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 = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
  • assert(cls->isMetaClass());这里一开始就有验证这个类是不是元类
  • 这里发送了一个SEL_resolveClassMethod给这个对象_class_getNonMetaClass(cls, inst),即给一个非元类对象(类),发送了一个类方法消息,点进去看这个对象生成的方法
Class _class_getNonMetaClass(Class cls, id obj)
{
    mutex_locker_t lock(runtimeLock);
    cls = getNonMetaClass(cls, obj);
    assert(cls->isRealized());
    return cls;
}

继续找getNonMetaClass

static Class getNonMetaClass(Class metacls, id inst)
{
    static int total, named, secondary, sharedcache;
    runtimeLock.assertLocked();

    realizeClass(metacls);

    total++;

    // return cls itself if it's already a non-meta class
    if (!metacls->isMetaClass()) return metacls;

    // metacls really is a metaclass

    // special case for root metaclass
    // where inst == inst->ISA() == metacls is possible
    if (metacls->ISA() == metacls) {
        Class cls = metacls->superclass;
        assert(cls->isRealized());
        assert(!cls->isMetaClass());
        assert(cls->ISA() == metacls);
        if (cls->ISA() == metacls) return cls;
    }

    // use inst if available
    if (inst) {
        Class cls = (Class)inst;
        realizeClass(cls);
        // cls may be a subclass - find the real class for metacls
        while (cls  &&  cls->ISA() != metacls) {
            cls = cls->superclass;
            realizeClass(cls);
        }
        if (cls) {
            assert(!cls->isMetaClass());
            assert(cls->ISA() == metacls);
            return cls;
        }
        //省略其他代码
  1. if (!metacls->isMetaClass()) return metacls; 如果已经是非元类,则返回class本身,不继续走之后的逻辑
  2. if (metacls->ISA() == metacls){...}如果这个元类的isa指向自己,而且它的superclass的isa也指向自己,则表示这个类是根元类,账直接返回
  • 这里应该怎么理解呢?我们看到下图,根元类的isa指针是指向自己的,它的superclassrootClass,然后它的isa指针也是指向这个根元类的root metaclass
    isa走位图
  1. 如果不是根元类,则继续找class的isa是指向元类的这个类cls->ISA(),而且这个类不能是子类
    if (inst) {
        Class cls = (Class)inst;
        realizeClass(cls);
        // cls may be a subclass - find the real class for metacls
        while (cls  &&  cls->ISA() != metacls) {
            cls = cls->superclass;
            realizeClass(cls);
        }
        if (cls) {
            assert(!cls->isMetaClass());
            assert(cls->ISA() == metacls);
            return cls;
        }
    }

_class_resolveInstanceMethod分析

回到刚才那里,再看到源码_class_resolveInstanceMethod的实现

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    //判断的目的是什么?
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    //调用的目的是为了什么?
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
    //省略下面无关代码
}
  • 首先lookUpImpOrNil的内部是通过lookUpImpOrForward方法进行查找,再次回到递归调用,lookUpImpOrForward_class_resolveMethod是互相循环调用的。

  • 如果还是没查到,这里就不会再次进入动态方法解析(注:如果再次进入动态方法解析会形成死递归),首先对cls的元类进行查找,然后元类的父类,也就是根元类(系统默认实现的虚拟的)进行查找、最终到NSObject,只不过NSObject中默认实现resolveInstanceMethod方法返回NO,也就是此时在元类进行查找的时候找到了resolveInstanceMethod方法,并停止继续查找,这就是为什么动态方法解析后的递归没有再次进入动态方法解析的原因。如果最终还是没有找到SEL_resolveInstanceMethod则说明程序有问题。

  • 再看到msg(cls, SEL_resolveInstanceMethod, sel);这里发送了一个SEL_resolveInstanceMethod给这个对象cls,即给一个元类,发送了一个实例方法消息

  • 这里该怎么理解呢?我们看到上面之前有一个结论:给一个非元类对象(类),发送了一个类方法消息。其实两者是一样的,因为对象方法存在于类中,以实例方法存在,类方法存在于元类中,也以实例方法存在。

  • 如下面的源码,获取一个类的类方法,其实就等于获取这个元类的实例方法,深刻理解上面的isa走位图

/***********************************************************************
* class_getClassMethod.  Return the class method for the specified
* class and selector.
**********************************************************************/
Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    return class_getInstanceMethod(cls->getMeta(), sel);
}

小结

非元类其实会沿着继承链,去找这个类有没有实现+resolveClassMethod。 如果没有,还会进行下一步补救措施,从它的元类里找有没有实现_class_resolveInstanceMethod,元类中没有找到就会去根元类中查找,一直查到NSObjct

动态解析方法的使用及作用

.h实现2个方法:

- (void)run;
+ (void)eat;

.m 没有实现上面的两个方法:

- (void)walk {
    NSLog(@"%s",__func__);
}
+ (void)drink {
    NSLog(@"%s",__func__);
}

// .m没有实现,并且父类也没有,那么我们就开启动态方法解析
//- (void)walk{
//    NSLog(@"%s",__func__);
//}
//+ (void)drink{
//    NSLog(@"%s",__func__);
//}


#pragma mark - 动态方法解析

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(run)) {
        // 我们动态解析我们的 对象方法
        NSLog(@"对象方法解析走这里");
        SEL walkSEL = @selector(walk);
        Method readM= class_getInstanceMethod(self, walkSEL);
        IMP readImp = method_getImplementation(readM);
        const char *type = method_getTypeEncoding(readM);
        return class_addMethod(self, sel, readImp, type);
    }
    return [super resolveInstanceMethod:sel];
}


+ (BOOL)resolveClassMethod:(SEL)sel{
    if (sel == @selector(eat)) {
        // 我们动态解析我们的 对象方法
        NSLog(@"类方法解析走这里");
        SEL drinkSEL = @selector(drink);
        // 类方法就存在我们的元类的方法列表
        // 类 类犯法
        // 元类 对象实例方法
        //        Method hellowordM1= class_getClassMethod(self, hellowordSEL);
        Method drinkM= class_getInstanceMethod(object_getClass(self), drinkSEL);
        IMP drinkImp = method_getImplementation(drinkM);
        const char *type = method_getTypeEncoding(drinkM);
        NSLog(@"%s",type);
        return class_addMethod(object_getClass(self), sel, drinkImp, type);
    }
    return [super resolveClassMethod:sel];
}

上面的代码就是利用动态方法解析的实例,那么它的作用有哪些呢?

适用于重定向,也可以做防崩溃处理,也可以做一些错误日志收集等等。动态方法解析本质就是提供机会(任何没有实现的方法都可以重新实现)

消息转发流程

在经历过上面的动态方法解析后,如果还没有找到,就会进入苹果尚未开源的消息转发流程。

 // No implementation found, and method resolver didn't help. 
    // Use forwarding.
    //_objc_msgForward
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

找到_objc_msgForward_impcache,发现是一个未实现的方法,也跟不进源码

#if !OBJC_OLD_DISPATCH_PROTOTYPES
extern void _objc_msgForward_impcache(void);
#else
extern id _objc_msgForward_impcache(id, SEL, ...);
#endif

很显然,去之前的汇编文件objc-msg-arm64.s里搜索,看到了汇编的实现,这里提到了一个__objc_forward_handler回调处理函数

ENTRY __objc_msgForward

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

在汇编里没有找到它的实现,然后在源码文件里找到了它,可以看到这个方法,打印的恰好就是我们调用未实现函数的崩溃信息...unrecognized selector...

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

走到这里,还是没有找到我们想看的方法转发流程,由于苹果这部分代码是尚未开源的,所以并不能看到源码实现,但是可以通过另一种方式instrumentObjcMessageSends来验证消息转发流程。

void instrumentObjcMessageSends(BOOL flag)
{
    bool enable = flag;

    // Shortcut NOP
    if (objcMsgLogEnabled == enable)
        return;

    // If enabling, flush all method caches so we get some traces
    if (enable)
        _objc_flush_caches(Nil);

    // Sync our log file
    if (objcMsgLogFD != -1)
        fsync (objcMsgLogFD);

    objcMsgLogEnabled = enable;
}

然后再看objcMsgLogFD,可以看到它会生成一个文件/tmp/msgSends-%d,存放日志

bool logMessageSend(bool isClassMethod,
                    const char *objectsClass,
                    const char *implementingClass,
                    SEL selector)
{
    char	buf[ 1024 ];

    // Create/open the log file
    if (objcMsgLogFD == (-1))
    {
        snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
        objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
        if (objcMsgLogFD < 0) {
            // no log file - disable logging
            objcMsgLogEnabled = false;
            objcMsgLogFD = -1;
            return true;
        }
    }

这个函数可以打印在objc消息过程中的所有日志,所以如下在调用run方法前开启日志打印

        instrumentObjcMessageSends(YES);
        [[Person alloc] run];
        instrumentObjcMessageSends(NO);

运行后,果然发现在文件夹/private/tmp多了一个msgSends-xxx的文件,打开可以看到里面的调用过程

再对比消息转发流程图,日志验证了这整个调用过程

消息转发流程的使用及作用

代码示例(实例对象消息转发):

#pragma mark - 实例对象消息转发
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
    //    if (aSelector == @selector(run)) {
    //        // 转发给Student对象
    //        return [Student new];
    //    }
    return [super forwardingTargetForSelector:aSelector];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
    if (aSelector == @selector(run)) {
        // forwardingTargetForSelector 没有实现,就只能方法签名了
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s",__func__);
    NSLog(@"------%@-----",anInvocation);
    anInvocation.selector = @selector(walk);
    [anInvocation invoke];
}

它的应用场景和动态方法解析类似,在异常捕获防崩溃,重定向,方法交换Method swizzling,切面编程的Aspects源码等都可以使用它。

总结

Runtime就是C、C++、汇编实现的一套API,给OC增加的一个运行时功能,也就是我们平时所说的运行时。 运行时: 在程序运行时,才会去确定对象的类型,并调用类与对象对应的方法。 Runtime的消息机制完美的提现了运行时的特征。

以上均为个人探索源码的理解和所得,如有错误请指正,欢迎讨论。