iOS 底层探索10——动态方法决议

454 阅读7分钟

前言

前面我们已经探究了整个objc的方法查找流程,包括快速查找:查找本类的cache慢速查找:查找当前类的methodlist和父类cache以及父类methodlist;如果经过快速查找和慢速查找都没有查到对应的信息,会怎么样呢?我们就在本文一起来探索下;

慢速查找回顾

慢速查找过程中会调用realizeAndInitializeIfNeeded_locked

  1. 慢速查找过程中会调用realizeAndInitializeIfNeeded_locked,这个方法内部会调用callInitialize, 这里相当于是调用classinitialize方法,
  2. 系统在启动过程中会调用+load+initializeC++构造方法会延长pre-main阶段的启动时间;
  3. 方法查找过程中

快速和慢速结合的方法查找

  1. 当前类通过查找cache快速查找不到方法时,会进入慢速查找:lookUpImpOrForward
  2. 当前类的慢速查找也找不到,就进入父类快速查找:cache_getImp;
  3. 如果父类快速查找也找不到,则进入父类慢速查找:lookUpImpOrForward,并依次往父类的父类查找,直到父类为空imp = forward_imp

方法动态决议

单例的实现

  1. 通过将两个数值先按位与按位异或,可以实现单例的效果;
if (slowpath(behavior & LOOKUP_RESOLVER)) {// 3 & 2 = 2
        behavior ^= LOOKUP_RESOLVER; //2 ^ 2 异或 相同为0  不同为1;  执行完后behavior = 0,以后再也无法满足上面判断了,相当于单例了;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

unrecognized selector sent打印的来源

  1. 在慢速查找lookUpImpOrForward中如果最终也找不到对应的方法就会进入imp = forward_imp;const IMP forward_imp = (IMP)_objc_msgForward_impcache;最后在done_unlock中把_objc_msgForward_impcache返回出来;
  2. 全局搜索_objc_msgForward_impcache,在汇编中看到 这个方法的定义是跳转到__objc_msgForward;
  3. 全局搜索__objc_msgForward这个方法最终跳转的是TailCallFunctionPointer 方法
	STATIC_ENTRY __objc_msgForward_impcache
	b	__objc_msgForward
	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
  1. 全局搜索TailCallFunctionPointer 最终发现是跳转$0,$0是上文的x17X17__objc_forward_handler@PAGE
  2. 全局搜索objc_forward_handler,在objc-runtime.mm找到 void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
  3. objc_defaultForwardHandler 的最终实现如下,这也解释了为什么找不到方法的时候会打印unrecognized selector sent to instance %p " "(no message forward handler is installed)等信息
__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);
}

消息处理流程

  1. 当方法找不到时会进入系统会自动进入消息转发流程,如图所示

消息转发流程.png

2.当方法找不到的时候会进入resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)方法;这个方法里面会执行resolveInstanceMethod方法,执行完resolveInstanceMethod方法后再次通过慢速查找去找缺失的方法;

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方法内部做了什么神奇的事情吗?为什么执行完resolveInstanceMethod方法后还会再次通过慢速查找去找缺失的方法呢?

  1. resolveInstanceMethod方法内部会去检查当前类是否有@selector(resolveInstanceMethod:);这个OC+方法,如果有就调用,那么开发者就可以在@selector(resolveInstanceMethod:);这个OC+方法里面添加查找不到的方法;如果没有找到@selector(resolveInstanceMethod:);这个OC+方法就直接返回了;正常情况下不会返回,因为即便我们自己的类不实现@selector(resolveInstanceMethod:);这个OC+方法,NSObject类中也有兜底实现;
  2. 调用@selector(resolveInstanceMethod:);这个OC+方法后,还会再次用慢速查找查一遍缺失的方法现在有没有,并且做标记;
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);
    //从当前类中查找@selector(resolveInstanceMethod:)这个oc方法,找不到直接返回
    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
        return;
    }
    //找到了@selector(resolveInstanceMethod:)这个oc方法就调用一次,开发者可以在这里做操作决定是否要动态添加缺失的方法
    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
    //这里主要是检查当发送了resolveInstanceMethod:后,缺失的方法是否可以找到了
    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));
        }
    }
}
  1. 至此,我们发现了消息转发流程的一个重要方法——动态方法决议:+resolveInstanceMethod:;方法在查找不到的时候就会进入类中的+resolveInstanceMethod:方法,而开发者可以在对应的class中的+resolveInstanceMethod:类方法里面,根据需要为类添加缺失的实例方法;

类方法动态方法决议+resolveClassMethod(inst, sel, cls);

  1. 为什么objc在类方法缺失的时候调用了resolveClassMethod(inst, sel, cls);而不是resolveInstanceMethod:呢;
  2. 为什么要特殊处理呢?因为正常情况下缺失的方法都是写在对应的类中,但是类方法是存储在元类中的,我们没有办法拿到元类,所以这里要特殊处理下,所以要用resolveClassMethod(inst, sel, cls);这个方法;
  3. 我们可以在对应的class中根据需要实现+resolveClassMethod:这个类方法,这样类方法在查找不到的时候就会进入类中的+resolveClassMethod:方法,而开发者可以在对应的class中的+resolveClassMethod:类方法里面,根据需要为类添加缺失的类方法;
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());
    //判断类中resolveClassMethod:这个oc方法是否存在;
    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));
        }
    }
}

通用动态方法决议

  1. 不管是类方法还是实例方法,本质上都是实例方法,只不过类方法是从元类中找实例方法,对象方法是从元类中找实例方法;
  2. 根据isa的走位图我们知道,不管是元类还是类,他们最终的父类都是NSObject;
  3. 我们可以通过向NSObject中添加+resolveInstanceMethod:方法,这样不管是对象方法缺失还是类方法缺失,最终都会找到NSObject中的+resolveInstanceMethod:方法,一次搞定对象方法和类方法,完美!

动态方法决议使用场景

  1. 通过向NSObject中添加category的形式,可以向NSObject中添加+resolveInstanceMethod:方法,可以监听到整个app所有的OC方法无法找到引起的崩溃并进行处理;

AOP&OOP

  1. 类似于在NSObject中的添加动态方法决议这种形式与我们平时常用的面向对象开发OOP面向对象是不同的,我们一般称为这种面向切面的低侵入性的为AOP模式;
  2. 由于AOP是无侵入的,可以在一定程度上对调用者和被调用者进行接耦,更加的灵活;
  3. AOP同时也会带来性能开销,例如我们的方法动态决议,如果使用AOP则需要一直向上到NSObject中才能查找到+resolveInstanceMethod:,不可避免的会浪费性能

oc方法log

  1. objc在调用插入cache方法:log_and_fill_cache()时会调用 logMessageSend()方法进行写入日志,日志输出的具体细节代码如下
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;
        }
    }

    // Make the log entry
    snprintf(buf, sizeof(buf), "%c %s %s %s\n",
            isClassMethod ? '+' : '-',
            objectsClass,
            implementingClass,
            sel_getName(selector));

    objcMsgLogLock.lock();
    write (objcMsgLogFD, buf, strlen(buf));
    objcMsgLogLock.unlock();

    // Tell caller to not cache the method
    return false;
}
  1. logMessageSend()是否记录日志是由 objcMsgLogEnabled这个变量来控制的,因此我们只需要修改objcMsgLogEnabled的值即可控制是否记录日志;
  2. 通过全局搜索,发现void instrumentObjcMessageSends(BOOL flag)方法可以修改objcMsgLogEnabled的值;如果我们想控制是否记录方法调用log,只需实现void instrumentObjcMessageSends(BOOL flag)方法即可;
  3. 通过以下代码如下最终实现了控制日志是否输出:
#import <Foundation/Foundation.h>
#import "GCPerson.h"

// 慢速查找 
extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        GCPerson *person = [GCPerson alloc];
        [GCPerson say];
        NSLog(@"Hello, World!");
    }
    return 0;
}
参考文章