iOS底层学习——动态方法决议

在前两篇文章objc_msgSend快速方法查找objc_msgSend慢速方法查找中,探究了函数调用的本质,即消息发送:objc_msgSend,并跟踪源码学习了方法查找的流程。本篇关注如果快速查找和慢速查找都没有找到方法怎么呢?

1.问题解析

根据前两篇文章,提出两个问题:

  1. forward_imp是什么?
  2. 如果方法找不到,如何补救?

1.forward_imp是什么?

在上面文章中,有过说明:如果方法未找到,即superclass一路找到了nil,仍未找到,则imp默认会被设置为forward_imp。那么forward_imp是什么呢?
在慢速查找流程lookUpImpOrForward方法的第一行代码,即对forward_imp进行了赋值:

const IMP forward_imp = (IMP)_objc_msgForward_impcache;
复制代码

此部分代码是通过汇编实现的,全局搜索__objc_msgForward_impcache,在objc_msg_arm64.s中查找到方法的实现:

        STATIC_ENTRY __objc_msgForward_impcache

	// No stret specialization.
	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
复制代码

汇编实现中查找__objc_forward_handler,并没有找到,在源码中去掉一个下划线进行全局搜索_objc_forward_handler,有如下实现,本质是调用的objc_defaultForwardHandler方法:

// 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;
复制代码

看上去很熟悉,没错就是我们日常开发中遇到的常见错误:函数未实现,运行程序崩溃时报的错误描述信息。

2.如果方法找不到,如何补救?

  • 动态方法决议:慢速查找流程未找到后,会执行一次动态方法决议。
  • 消息转发:如果动态方法决议仍然没有找到实现,则进行消息转发。消息转发分为:快速消息转发、慢速消息转发

2.动态方法决议

在上一篇文章慢速方法查找中,当superclass = nil,跳出循环,紧接着会再给一次机会,即动态方法决议,重新定义你的方法实现。

1.动态方法决议源码分析

lookUpImpOrForward中有下面一段代码,即动态方法决议入口:

    // No implementation found. Try method resolver once.

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

slowpath(behavior & LOOKUP_RESOLVER)可以理解为一个开关阀,保证动态方法决议只会执行一次!进入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 (!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);
}
复制代码

流程分析:

  1. 判断cls的类型;
  2. 如果是类,则执行实例方法的动态决议resolveInstanceMethod方法。
  3. 如果是元类,则执行类方法的动态决议resolveClassMethod方法。如果元类中没有找到该实例方法或者为空,则在元类的实例方法的动态方法决议resolveInstanceMethod中查找。为什么呢?因为类方法在元类中,是以对象方法的形式存储,所以需要执行元类的实例对象决议方法。也就是说类是元类的实例对象
  4. 如果方法决议将方法的实现指向了其他地方,则继续执行最后一行的lookUpImpOrForwardTryCache方法,进行方法查找流程,并返回imp

2.resolveInstanceMethod源码分析

对象方法动态方法决议会调用resolveInstanceMethod方法。源码如下:

image.png

流程解析:

  1. A处:进行慢速方法查找,判断类是否实现了resolveInstanceMethod方法;如果没有找到,直接返回;
  2. B处:如果找到,则发送消息,执行resolveInstanceMethod方法;
  3. C处:再次进行方法查找,即通过_lookUpImpTryCache方法进入lookUpImpOrForward进行慢速方法查找。

连续多次的方法查找,很混乱,每一步都做了什么呢?下面用案例进行探索分析。

3.案例初步探索

LGPerson类的声明中添加两个方法,-(void)sayHello;-(void)sayHello1;;类实现中,重写resolveInstanceMethod方法,并实现方法-(void)sayHello1,而-(void)sayHello并未实现。

@interface LGPerson : NSObject
-(void)sayHello;
-(void)sayHello1;

+ (void)say666;
+ (void)say6661;
@end


@implementation LGPerson

- (void)sayHello1{
    NSLog(@"sayHello1 %s", __func__);
}

+ (void)say6661{
    NSLog(@"say6661 %s", __func__);
}

// 动态方法决议 - 给一次机会, 需要进行方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    
    NSLog(@"给你一次机会...");

    // 什么也没做
    return 0;
}
@end
复制代码

案例运行结果:

image.png

案例解析:
虽然重写了动态方法决议方法resolveInstanceMethod,但是依然报错,并且该方法还被调用了两次。为什么呢?下面进行代码跟踪调试。

  1. 再次运行上面的案例,过滤出我们需要研究的内容,即LGPerson对象调用sayHello方法,进入动态方法决议方法resolveInstanceMethod。见下图:

    image.png

  2. 判断cls,也就是LGPerson,是否实现了resolveInstanceMethod类方法。见下图:

    image.png

  3. 在元类中进行方法查找,是否实现了resolveInstanceMethod,路径为:lookUpImpOrNilTryCache -> _lookUpImpTryCache -> lookUpImpOrForward,见下图:

    image.png

  4. 在方法列表中,成功找到resolveInstanceMethod方法,并插入缓存。

    image.png

  5. 如果没有找到,此处会直接返回,即没有利用这次机会,直接返回!而如果找到则发送一条resolveInstanceMethod消息,即执行resolveInstanceMethod方法

    image.png

  6. 完成消息发送后,会再进行sayHello方法的查找,但是依然找不到!因为LGPerson虽然实现了resolveInstanceMethod,但是里面什么也没有做

    image.png

  7. resolveInstanceMethod执行完成后,回到resolveMethod_locked流程中,调用lookUpImpOrForwardTryCache再次进行方法查找。

    image.png

  8. lookUpImpOrForwardTryCache中,依然查找sayHello,此时会从缓存中返回forward_imp,也就是进行消息转发

    image.png

  9. 动态方法决议流程结束,只是此案例中,虽然实现了动态方法决议,但是里面什么也没有做,进入消息转发,最终运行结果报错!

总结:通过上面的案例,理清楚了动态方法决议的流程。但是实际开发过程中,我们肯定会抓出这次处理错误的机会。下面我们对案例进行修改,将sayHello方法指向其他方法。

4.案例深入探索

依然是上面的案例,但是我们抓住这次机会,向类中添加一个方法,方法的sel依然是sayHello,但是其对应的方法实现impsayHello1的实现。

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    
    NSLog(@"给你一次机会...");
    if (sel == @selector(sayHello))
    {
        IMP imp = class_getMethodImplementation(self, @selector(sayHello1));
        Method method = class_getInstanceMethod(self, @selector(sayHello1));
        class_addMethod(self, sel, imp, method_getTypeEncoding(method));

        return NO;
    }
    return [super resolveInstanceMethod:sel];
}
复制代码

运行上面的代码,这次有什么不同呢?我们依然把关注点放到resolveInstanceMethod方法中,跟踪ABC三个地方分别作了什么?

image.png

  1. 调用LGPerson类的实例方法sayHello,分别进行快速查找和慢速查找,均找不到该方法。最终会进入源码的resolveMethod_locked -> resolveInstanceMethod流程中。

  2. 代码运行到A处,会查找类是否实现了resolveInstanceMethod,如果实现了,则会将该方法插入缓存,以便下次进行快速方法查找;如果没有实现,直接返回。此处流程和初探时的流程是一致的!

  3. 代码运行到B处,发送msg,即执行LGPerson 类中的resolveInstanceMethod方法。由于将sayHello方法指向了sayHello1,则此处class_addMethod会将方法插入class_rw_ext_t,也就是插入LGPerson的方法列表中;

    方法插入流程,见下图:

    image.png

    至此,动态方法决议方法已经执行一次,并重新设定了方法实现。

  4. 代码运行到C处,再次查找sayHello,此流程会在方法列表中查找到方法实现sayHello1,并以sel=sayHelloimp=sayHello1实现的形式插入方法缓存。

    • C处调用路径:lookUpImpOrNilTryCache -> _lookUpImpTryCache -> lookUpImpOrForward

    image.png

    • 进入lookUpImpOrForward,查找sayHello方法。

    image.png

    最终在方法列表中找到了,并将其插入缓存中。

  5. 继续运行代码,回到resolveMethod_locked,并再次调用lookUpImpOrForwardTryCache方法,进行方法查找。

    image.png

    此时,通过cache_getImp找到了方法实现,方法实现为sayHello1,返回imp

    image.png

疑问说明:

resolveInstanceMethod返回NO,才接着走后面的转发流程,而返回YES就停止转发了?

  • 其实如果重写的resolveInstanceMethod什么也不做,只是返回YES也会接着走后面的转发流程。这个返回值对于消息转发流程没有任何意义,从runtime的源码来看这个返回值只和debug的信息相关。

5.类方法动态决议

针对类方法,与实例方法类似,同样可以通过重写resolveClassMethod类方法来解决前文的崩溃问题,即在LGPerson类中重写该方法,并将say666类方法的实现指向类方法say6661

+(BOOL)resolveClassMethod:(SEL)sel{
    NSLog(@"给你一次机会...+++");
    if (sel == @selector(say666))
    {
        Class meteCls = objc_getMetaClass("LGPerson");
        IMP imp = class_getMethodImplementation(meteCls, @selector(say6661));
        Method method = class_getInstanceMethod(meteCls, @selector(say6661));
        return class_addMethod(meteCls, sel, imp, method_getTypeEncoding(method));
    }
    return [super resolveClassMethod:sel];
}
复制代码

使用说明:

resolveClassMethod类方法的重写需要注意一点,传入的cls不再是类,而是元类,可以通过objc_getMetaClass方法获取类的元类,原因是因为类方法在元类中是实例方法。

3.动态方法决议使用优化

上面的这种方式是单独在每个类中重写,有没有更好的,一劳永逸的方法呢?其实通过方法慢速查找流程可以发现其查找路径有两条:

  • 实例方法类 -- 父类 -- 根类 -- nil
  • 类方法元类 -- 根元类 -- 根类 -- nil

它们的共同点是如果前面没找到,都会来到根类NSObject中查找,所以我们是否可以将上述的两个方法统一整合在一起呢?答案是可以的,可以通过NSObject添加分类的方式来实现统一处理,而且由于类方法的查找,在其继承链,查找的也是实例方法,所以可以将实例方法类方法的统一处理放在resolveInstanceMethod方法中,如下所示:

// NSObject分类
@implementation NSObject (GF)

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(sayHello)) {
        NSLog(@"%@ 给你一次机会...", NSStringFromSelector(sel));
        
        IMP imp = class_getMethodImplementation(self, @selector(sayHello1));
        Method sayMethod  = class_getInstanceMethod(self, @selector(sayHello1));
        const char *type = method_getTypeEncoding(sayHello1);
        return class_addMethod(self, sel, imp, type);
    }else if (sel == @selector(say666)) {
        NSLog(@"%@ 给你一次机会+++", NSStringFromSelector(sel));
        
        IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(say6661));
        Method lgClassMethod  = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(say6661));
        const char *type = method_getTypeEncoding(say6661);
        return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
    }
    return NO;
}
@end

复制代码

这种方式的实现,正好与源码中针对类方法的处理逻辑是一致的,即完美阐述为什么调用了类方法动态方法决议,还要调用对象方法动态方法决议,其根本原因还是类方法在元类中的实例方法

当然,上面这种写法还是会有其他的问题,比如系统方法也会被更改,针对这一点,是可以优化的,即我们可以针对自定义类中方法统一方法名的前缀,根据前缀来判断是否是自定义方法,然后统一处理自定义方法,例如可以在崩溃前pop到首页,主要是用于app线上防崩溃的处理,提升用户的体验。

4.动态方法决议执行两次探索

以对象方法决议resolveInstanceMethod为例,我们可以写个示例测试一下,调用一个未实现的SEL,并重写resolveInstanceMethod,但是不对方法进行重定向,然而发现,这个方法竟然被调用了2次

见下面的示例截图:

image.png

从上面的案例结果中可以发现,resolveInstanceMethod动态决议方法中给你一次机会...打印了两次,这是为什么呢?

通过bt查看堆栈信息可以看出:

image.png

  • 第一次动态决议:第一次,和我们分析的是一致的,是在查找sayHello方法时没有找到,会进入动态方法决议,发送resolveInstanceMethod消息

  • 第二次动态决议:第二次,是在调用了CoreFoundation框架中的NSObject(NSObject) methodSignatureForSelector:后,会再次进入动态决议。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享