iOS底层原理—动态方法决议和消息转发

192 阅读14分钟

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方法。源码如下:

1.png 流程解析:

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

3.案例初步探索

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

@interface JHSPerson : NSObject

- (void)saySomething1;

- (void)saySomething2;

@end

#import "JHSPerson.h"
@implementation JHSPerson

- (void)saySomething1{

    NSLog(@"saySomething1 %s",__func__);

}
// 动态方法决议 - 给一次机会, 需要进行方法实现

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSLog(@"给你一次机会...");
    // 什么也没做
    return 0;
}
@end

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

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

3.png

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

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

5.png

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

6.png

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

7.png

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

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

9.png

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

10.png

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

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

4.案例深入探索

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

// 动态方法决议 - 给一次机会, 需要进行方法实现

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSLog(@"给你一次机会...");

    if (sel == @selector(saySomething2)) {

        IMP imp = class_getMethodImplementation(self, @selector(saySomething1));

        Method method = class_getInstanceMethod(self, @selector(saySomething1));

        class_addMethod(self, sel, imp, method_getTypeEncoding(method));
    }
    return [super resolveInstanceMethod:sel];

}

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

11.png

  1. 调用JHSPerson类的实例方法saySomething2,分别进行快速查找和慢速查找,均找不到该方法。最终会进入源码的resolveMethod_locked -> resolveInstanceMethod流程中。
  2. 代码运行到1处,会查找类是否实现了resolveInstanceMethod,如果实现了,则会将该方法插入缓存,以便下次进行快速方法查找;如果没有实现,直接返回。此处流程和初探时的流程是一致的!
  3. 代码运行到2处,发送msg,即执行JHSPerson 类中的resolveInstanceMethod方法。由于将saySomething2方法指向了saySomething1,则此处class_addMethod会将方法插入class_rw_ext_t,也就是插入JHSPerson的方法列表中; 方法插入流程,见下图:

12.png

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

  1. 代码运行到3处,再次查找saySomething2,此流程会在方法列表中查找到方法实现saySomething1,并以sel=saySomething2, imp=saySomething1实现的形式插入方法缓存。
  • 3处调用路径:lookUpImpOrNilTryCache -> _lookUpImpTryCache -> lookUpImpOrForward

13.png

  • 进入lookUpImpOrForward,查找sayHello方法。

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

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

15.png

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

16.png

5.类方法动态决议

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

// 动态方法决议 - 给一次机会, 需要进行方法实现

+ (BOOL)resolveInstanceMethod:(SEL)sel{

    

    NSLog(@"给你一次机会...");

    if (sel == @selector(saySomething2)) {

        IMP imp = class_getMethodImplementation(self, @selector(saySomething1));

        Method method = class_getInstanceMethod(self, @selector(saySomething1));

        class_addMethod(self, sel, imp, method_getTypeEncoding(method));

    }

    if (sel == @selector(saySomething3)) {

        Class meteCls = objc_getMetaClass("JHSPerson");

        IMP imp = class_getMethodImplementation(meteCls, @selector(saySomething4));

        Method method = class_getInstanceMethod(meteCls, @selector(saySomething4));

        return class_addMethod(meteCls, sel, imp, method_getTypeEncoding(method));

    }

    // 什么也没做

    return [super resolveInstanceMethod:sel];

}

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

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

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

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

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(saySomething2)) {

        IMP imp = class_getMethodImplementation(self, @selector(saySomething1));

        Method method = class_getInstanceMethod(self, @selector(saySomething1));

        class_addMethod(self, sel, imp, method_getTypeEncoding(method));

    }

    if (sel == @selector(saySomething3)) {

        Class meteCls = objc_getMetaClass("JHSPerson");

        IMP imp = class_getMethodImplementation(meteCls, @selector(saySomething4));

        Method method = class_getInstanceMethod(meteCls, @selector(saySomething4));

        return class_addMethod(meteCls, sel, imp, method_getTypeEncoding(method));

   }
    
    return NO;
}
@end

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

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

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

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

17.png

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

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

18.png

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

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

5.消息转发探索

从上面的流程中了解到,objc_msgSend在完成快速查找和慢速查找后,均没有找到方法,就会进行动态方法决议,如果动态方法决议也没有处理,则会进行消息转发流程。但是,我们找遍了也没有发现消息转发的相关源码。 通过instrumentObjcMessageSends方式打印发送消息的日志。在慢速方法查找的插入缓存流程中:log_and_fill_cache -> logMessageSend,找到了instrumentObjcMessageSends源码实现。如果要打印消息发送运行日志,首先需要控制objcMsgLogEnabledtrue,同时能够在发送消息的地方调用instrumentObjcMessageSends方法才行。根据以上两点,完成下面的案例:

@interface JHSPerson : NSObject

- (void)saySomething1;

- (void)saySomething2;

@end

@implementation JHSPerson

- (void)saySomething1{

    NSLog(@"saySomething1");

}

@end

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {

    @autoreleasepool {

        instrumentObjcMessageSends(YES);

        JHSPerson *person = [JHSPerson alloc];

        [person saySomething2];

        instrumentObjcMessageSends(NO);

        NSLog(@"Hello, World!");

    }

    return 0;

}

志打印在哪里呢?在logMessageSend 源码实现中,已经告诉我们了。 snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ()); 运行以上代码,然后Finder中前往/tmp/msgSends/,就可以找到运行日志,见下图:

19.png

  • 动态方法决议:resolveInstanceMethod
  • 快速消息转发:forwardingTargetForSelector
  • 慢速消息转发:methodSignatureForSelector

6.快速消息转发

通过日志我们了解到forwardingTargetForSelector对象方法实际调用者是JHSPerson对象,所以在JHSPerson类中添加对象方法的forwardingTargetForSelector实现,依然调用JHSPerson的对象方法saySomething2

// 动态方法决议 - 给一次机会, 需要进行方法实现

+ (BOOL)resolveInstanceMethod:(SEL)sel{

    

    NSLog(@"给你一次机会...");

//    if (sel == @selector(saySomething2)) {

//        IMP imp = class_getMethodImplementation(self, @selector(saySomething1));

//        Method method = class_getInstanceMethod(self, @selector(saySomething1));

//        class_addMethod(self, sel, imp, method_getTypeEncoding(method));

//    }

//    if (sel == @selector(saySomething3)) {

//        Class meteCls = objc_getMetaClass("JHSPerson");

//        IMP imp = class_getMethodImplementation(meteCls, @selector(saySomething4));

//        Method method = class_getInstanceMethod(meteCls, @selector(saySomething4));

//        return class_addMethod(meteCls, sel, imp, method_getTypeEncoding(method));

//    }

    return [super resolveInstanceMethod:sel];

}

// 快速消息转发

- (id)forwardingTargetForSelector:(SEL)aSelector {

    NSLog(@"给你二次机会...%@", NSStringFromSelector(aSelector));

    return [super forwardingTargetForSelector:aSelector];

}

运行结果发现,依然崩溃,因为依然没有找到方法实现。但是在快速消息转发中的打印结果为:给你二次机会...saySomething2。也就是说在错过第一次补救机会动态方法决议后,快速消息转发forwardingTargetForSelector会给我们第二次的补救机会。 这次机会我们可以理解为甩锅,简单理解为:我处理不了了,你让别人帮我处理吧! 现在对上面的案例进行一些修改,添加一个JHSTeacher类,声明并实现saySomething5方法

@interface JHSTeacher : NSObject

- (void)saySomething2;

- (void)saySomething6;

@end

@implementation JHSTeacher

- (void)saySomething2{

    NSLog(@"saySomething2 %s",__func__);

}

- (void)saySomething6{

    NSLog(@"saySomething3 %s",__func__);

}

@end

JHSPerson类的快速消息转发中,将方法甩锅给JHSTeacher对象。也就是抓住这次机会,重新设置一个方法接受者。

// 快速消息转发

- (id)forwardingTargetForSelector:(SEL)aSelector {

    NSLog(@"给你二次机会...%@", NSStringFromSelector(aSelector));

    return [JHSTeacher alloc];

//    return [super forwardingTargetForSelector:aSelector];

}

运行后不再崩溃,并成功调用了JHSTeacher中的saySomething2方法。说明这次甩锅起到了效果,将方法的接受者变成了JHSTeachern对象。需要说明的是,在JHSTeacher中寻找saySomething2方法时,依然会走快速查找、慢速查找、动态方法决议等流程。

20.png

7.慢速消息转发

根据上面的流程我们知道,慢速消息转发流程调用了methodSignatureForSelector方法。在苹果官方文档中搜索methodSignatureForSelector方法的使用说明,发现需要配合invocation使用,即需要实现forwardInvocation方法。见下图:

21.png

继续上面的案例,不在快速消息转发forwardTargetForSelector方法中做任何处理。

// 慢速消息转发

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{

    NSLog(@"给你三次机会...%@", NSStringFromSelector(aSelector));

    return [super methodSignatureForSelector:aSelector];

}

- (void)forwardInvocation:(NSInvocation *)anInvocation{

}

成功走到了methodSignatureForSelector方法中,但是依然崩溃。下面做一些修改,在该方法中返回一个方法签名

// 如果走到这一步,动态方法决议会走两次
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"给你三次机会...%@", NSStringFromSelector(aSelector));
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
//    return [super methodSignatureForSelector:aSelector];
}

打印结果发现,forwardInvocation方法中即使不对invocation事务进行处理,也不会崩溃报错了。

22.png 在快速消息转发中,只可修改方法的接受者;而在慢速消息转发中可以重新设置方法、接受者等,更加灵活,权限更大

在方法forwardInvocation方法中做一些修改,重新设置事务的target = JHSTeacher、selector = saySomething6

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

运行结果见下图。不崩溃,并且从一开始的调用JHSPerson对象的saySomething2,到成功调用JHSTeacher对象的saySomething6方法

23.png

8.总结

到目前为止,objc_msgSend发送消息的流程就分析完成了,在这里简单总结下。

  • 快速查找流程:首先,在类的缓存cache中查找指定方法的实现。
  • 慢速查找流程:如果缓存中没有找到,则在类的方法列表中查找,如果还是没找到,则去父类链的缓存和方法列表中查找。
  • 动态方法决议:如果慢速查找还是没有找到时,第一次补救机会就是尝试一次动态方法决议,即重写resolveInstanceMethod/resolveClassMethod 方法
  • 消息转发:如果动态方法决议还是没有找到,则进行消息转发,消息转发中有两次补救机会:快速转发、慢速转发
  • 如果转发之后也没有,则程序直接报错崩溃unrecognized selector sent to instance