OC底层原理(十):objc_msgSend的动态方法决议与消息转发

11,312 阅读21分钟

在前一篇文章objc_msgSend慢速方法查找中,探究了消息慢速查找,即消息发送objc_msgSend快速查找进入到慢速查找,并跟踪源码学习了方法慢速查找的流程。本篇关注如果快速查找慢速查找都没有找到方法怎么办?就是上一篇遗留下来的动态方法决议消息转发

一、慢速查找遗留的两个问题

  • 慢速方法查找c++函数lookUpImpOrForward中,无论是在当前类class还是父类superclass缓存cache中还是类方法列表methods中只要找对应imp就会直接返回结果;但是都找不到就会根据情况先进入resolveMethod_locked,再执行forward_imp

  • forward_imp是什么?与resolveMethod_locked是什么?什么情况触发与工作流程?

forward_imp是什么?

在函数lookUpImpOrForward中,如果方法imp未找到,即superclass一路找到了nil;从当前类到父类再到NSObject最后到NSObject的空父类仍未找到,则imp默认会被设置为forward_imp。那么forward_imp是什么呢?

  1. 慢速查找流程的lookUpImpOrForward函数的第一行代码,即对forward_imp进行了赋值:
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
   ...
   ...
}
  1. 此部分代码是通过汇编实现的,全局搜索__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
  1. 汇编实现中查找__objc_forward_handler,并没有找到,在源码中去掉一个下划线_进行全局搜索_objc_forward_handler;在objc-runtime.mm中找到,该方法本质是调用的objc_defaultForwardHandler方法:
void *_objc_forward_handler = (void*)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);
}
  • 结论:

看上去很熟悉,没错就是我们日常开发中遇到的常见错误:函数未实现,运行程序崩溃时报的错误描述信息。 所以forward_imp负责打印未找到该方法的内容。

resolveMethod_locked是什么?

superclass = nil,跳出循环,紧接着会再给一次机会,即动态方法决议,重新定义你的方法实现。

NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    ...
    // No implementation found. Try method resolver once.
    /** 
    * 如果遍历查找的过程找到了,会跳过此步骤,取到done分支,进行后续操作 
    * 如果找不到,会进行下面这个算法,最终进入动态方法决议resolveMethod_locked函数 
    * 此算法真正达到的目的为单例,保证一个lookUpImpOrForward 
    * 只执行一次resolveMethod_locked 
    **/
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }
    ...
}
  • 结论:

slowpath(behavior & LOOKUP_RESOLVER)可以理解为一个开关阀,保证动态方法决议只会执行一次!直到behavior被重新赋值!进入resolveMethod_locked函数就是动态方法决议,可以详细了解动态方法决议的流程。

二、动态方法决议resolveMethod_locked

但是如果当前类与父类的cache缓存methods方法列表都没有时,当superclass = nil,跳出循环,紧接着会再给一次机会进入resolveMethod_locked,即动态方法决议,重新定义你的方法实现

resolveMethod_locked方法

当你调用了一个方法的时候,第一进入消息快速查找流程 -> 然后进入消息慢速查找流程,当底层源码已经给你方法查找了2遍之后依然找不到你实现的地方;此时imp=nil,理论上来讲程序应该崩溃,但是在开发者的角度上来讲,此做法会令这个框架不稳定,或者说这个系统很不友善。 所以此框架决定再给你一次机会,为你提供了一个自定义的imp返回的机会,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()) {
        //imp为实例方法
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        //imp为类方法
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // 经过resolveInstanceMethod函数很有可能已经对sel对应imp完成了动态添加
    // 所以再一次尝试查找
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
  • 结论:
  1. 此函数里面有三个关键的函数:
    • resolveInstanceMethod:实例方法动态添加imp
    • resolveClassMethod:类方法动态添加imp
    • lookUpImpOrForwardTryCache:当完成添加之后,回到之前的慢速查找流程再来一遍。

resolveInstanceMethod方法

对象方法动态方法决议会调用resolveInstanceMethod方法,处理的是对象的实例方法。

  • 实例方法动态添加:
/*********************************************************
* 解析实例方法
* 调用+resolveInstanceMethod,寻找要添加到类cls的方法。
* cls 可能是元类或非元类。
* 不检查该方法是否已经存在。
*********************************************************/
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    //当你为实现resolveInstanceMethod的时候,此处也不会进入return
    //因为系统给resolveInstanceMethod函数默认返回NO,即默认实现了
    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        return;
    }

    //系统会在此处为你发送一个消息resolve_sel
    //当你的这个类检测了这个消息,并且做了处理
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);

    //那么此时系统会重新查找,此函数最终会触发LookUpImpOrForward
    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));
        }
    }
}

resolveClassMethod方法

类方法动态方法决议会调用resolveClassMethod方法,处理的是元类对象的方法。

  • 类方法动态添加:
/*********************************************************
* 解析类方法
* 调用+resolveClass 方法,寻找要添加到类cls 的方法。
* cls 应该是一个元类。
* 不检查该方法是否已经存在。
*********************************************************/
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());

    //当你为实现resolveClassMethod的时候,此处也不会进入return
    //因为系统给resolveClassMethod函数默认返回NO,即默认实现了
    if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
        // Resolver not implemented.
        return;
    }
    
    //nonmeta容错处理
    Class nonmeta;
    {
        mutex_locker_t lock(runtimeLock);
        nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
        if (!nonmeta->isRealized()) {
            _objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
                        nonmeta->nameForLogging(), nonmeta);
        }
    }
    //系统会在此处为你发送一个消息resolveClassMethod
    //当你的这个类检测了这个消息,并且做了处理
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);

    //那么此时系统会重新查找,此函数最终会触发LookUpImpOrForward
    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));
        }
    }
}

resolveClassMethod & resolveInstanceMethod

NSObject已经在NSObject.mm默认实现这两个类方法。一般都是自定义复写这两个方法,来动态添加方法。

//默认返回NO,当用户不实现这个方法的时候,程序也不会return
+ (BOOL)resolveClassMethod:(SEL)sel {
    return NO;
}
//默认返回NO,当用户不实现这个方法的时候,程序也不会return
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;
}

lookUpImpOrForwardTryCache过度函数

这个是动态方法决议后,重新查找方法情况。

IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{
    return _lookUpImpTryCache(inst, sel, cls, behavior);
}

_lookUpImpTryCache

通过动态添加方法之后,再次尝试查找sel对应的最新添加的imp

ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertUnlocked();
    
    //当类未初始化的时候,进入lookUpImpOrForward
    //在里面处理不缓存任何方法
    if (slowpath(!cls->isInitialized())) {
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }
    
    //通过汇编查找
    IMP imp = cache_getImp(cls, sel);
    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:
    //此判断是当前imp已经存在了,并且这个imp是默认赋值的forward_imp
    //此时返回imp为nil;
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
        return nil;
    }
    return imp;
}
  • 结论:

那这些函数有什么用处?resolveMethod_lockedresolveInstanceMethod函数都会执行lookUpImpOrNilTryCache,为什么要执行2遍呢?那接下来用复写类方法resolveInstanceMethod探索流程。

实例方法的动态决议

按照源码还原,通过源码已得知会给开发者一次修复的机会,通过resolveInstanceMethod这个方法,这里在自定义类CJPerson复写一下这个方法。

  • 测试代码:
@interface CJPerson : NSObject
- (void)sayHello;
- (void)sayHello1;

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

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

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

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

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CJPerson *p = [[CJPerson alloc]init];
        [p sayHello];
    }
    return 0;
}
  • 控制台打印结果:
2023-01-08 15:54:27.966872+0800 KCObjcBuild[59458:17979158] 给你一次机会...
warning: KCObjcBuild was compiled with optimization - stepping may behave oddly; variables may not be available.
2023-01-08 15:54:37.901855+0800 KCObjcBuild[59458:17979158] 给你一次机会...
2023-01-08 15:55:05.431298+0800 KCObjcBuild[59458:17979158] -[CJPerson sayHello]: unrecognized selector sent to instance 0x600003004000
2023-01-08 15:55:08.244166+0800 KCObjcBuild[59458:17979158]  Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[CJPerson sayHello]: unrecognized selector sent to instance 0x600003004000'
*** First throw call stack:
(
    0   CoreFoundation                      0x00007ff8100d543b __exceptionPreprocess + 242
    1   libobjc.A.dylib                     0x000000010070fb8a objc_exception_throw + 42
    2   CoreFoundation                      0x00007ff81016c56b -[NSObject(NSObject) __retain_OA] + 0
    3   CoreFoundation                      0x00007ff81003f69b ___forwarding___ + 1324
    4   CoreFoundation                      0x00007ff81003f0d8 _CF_forwarding_prep_0 + 120
    5   KCObjcBuild                         0x00000001000035d0 main + 64
    6   dyld                                0x00007ff80fc51310 start + 2432
)
libc++abi: terminating with uncaught exception of type NSException
warning: could not find Objective-C class data in the process. This may reduce the quality of type information available.

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[CJPerson sayHello]: unrecognized selector sent to instance 0x600003004000'

terminating with uncaught exception of type NSException
  • 过程:

    依然报错是因为此时方法sayHello还并未实现,但是在崩溃之前打印了手动介入的信息,也就是说,在崩溃之前有补救的办法。但是过程中resolveInstanceMethod执行了两次。通过bt指令在终端打印内存情况!

截屏2023-01-12 下午2.53.34 2.png

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

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

探索resolveInstanceMethod动态方法决议流程

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

  1. 再次运行上面的案例,过滤出我们需要研究的内容,即CJPerson对象调用sayHello方法,第一次进入动态方法决议方法resolveInstanceMethod
  • 图: 截屏2023-01-08 下午4.34.26.png
  1. 判断cls,也就是CJPerson,是否实现了resolveInstanceMethod类方法
  • 图: 截屏2023-01-08 下午4.41.53.png
  1. 元类中进行方法查找(即查找类方法),是否实现了类方法resolveInstanceMethod,路径为:lookUpImpOrNilTryCache -> _lookUpImpTryCache -> lookUpImpOrForward
  • 图: 截屏2023-01-08 下午4.47.50.png
  1. 方法列表methods中,成功找到resolveInstanceMethod方法,并插入缓存
  • 图: 截屏2023-01-08 下午4.53.22.png
  1. 如果没有找到,此处会直接返回,即没有利用这次机会,直接返回!而如果找到则发送一条resolveInstanceMethod消息,即执行resolveInstanceMethod方法。
  • 图: 截屏2023-01-08 下午5.17.27.png
  1. 完成消息发送后,会再进行sayHello方法的查找,但是依然找不到!因为CJPerson虽然实现了resolveInstanceMethod,但是里面什么也没有做
  • 图: 截屏2023-01-08 下午5.23.36.png
  1. resolveInstanceMethod执行完成后,回到resolveMethod_locked流程中,调用lookUpImpOrForwardTryCache再次进行方法查找
  • 图: 截屏2023-01-08 下午5.27.08.png
  1. lookUpImpOrForwardTryCache中,依然没有查找到sayHello,此时会从缓存中返回forward_imp
  • 图: 截屏2023-01-08 下午5.32.17.png
  1. 最后动态方法决议流程结束,只是此案例中,虽然实现了动态方法决议,但是里面什么也没有做,进入消息转发,最终运行结果报错!
  • 图: 截屏2023-01-08 下午5.43.36.png

  • 结论:

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

使用resolveInstanceMethod动态方法决议

依然是上面的案例,如果我们向类中添加一个方法,方法的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));
        const char *type = method_getTypeEncoding(method);

        return class_addMethod(self, sel, imp, type);
    }
    return [super resolveInstanceMethod:sel];
}
  • 注意点:

objc4.886会报错!因为这个版本的源码将addMethod函数改成了只适配arm64ebigSigned();由于调试源码是macOS环境,将bigSigned()改回big();如果是iOS真机不需要更改。

截屏2023-01-11 下午3.24.34.png

使用动态决议流程探索

运行复写类方法resolveInstanceMethod方法的代码,这次有什么不同呢?我们依然把关注点放到resolveInstanceMethod方法中,跟踪①、②、③三个地方分别做了什么?

截屏2023-01-11 下午4.12.59.png

调用CJPerson类的实例方法sayHello,分别进行快速查找慢速查找,均找不到该方法。最终会进入源码动态方法决议resolveMethod_locked -> resolveInstanceMethod流程中:

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

  • 运行到②,发送msg,即执行CJPerson 类中的resolveInstanceMethod方法。由于将sayHello方法指向了sayHello1,则此处class_addMethod会将方法插入class_rw_ext_t,也就是插入CJPerson方法列表中。

  • 运行到③,再次查找sayHello,此流程会在方法列表中查找到方法实现sayHello1,并以sel=sayHello, imp=sayHello1实现的形式插入方法缓存。

    • ③调用路径:lookUpImpOrNilTryCache -> _lookUpImpTryCache -> lookUpImpOrForward 截屏2023-01-11 下午5.27.10.png
    • 进入lookUpImpOrForward,查找sayHello方法 截屏2023-01-11 下午5.35.25.png
    • 最终在方法列表中找到了,并将其插入缓存中。
    • 继续运行代码,回到resolveMethod_locked,并再次调用lookUpImpOrForwardTryCache方法,进行方法查找截屏2023-01-11 下午5.49.41.png
    • 此时,通过cache_getImp找到了方法实现,方法实现为sayHello1,返回imp
      截屏2023-01-11 下午5.57.16.png

类方法的动态决议

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

+(BOOL)resolveClassMethod:(SEL)sel{
    NSLog(@"给你一次机会...+++");
    if (sel == @selector(say666))
    {
        Class meteCls = objc_getMetaClass("CJPerson");
        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];
}

动态方法决议使用优化

上面的这种实现方式就是是单独在每个类中重写resolveInstanceMethod/resolveClassMethod,太麻烦了也不好管理。其实通过方法慢速查找流程可以发现其查找路径有两条:

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

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

// NSObject分类
#import "NSObject+CJ.h"
#import <objc/message.h>
@implementation NSObject (CJ)

+ (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("CJPerson"), @selector(say6661));
        Method lgClassMethod  = class_getInstanceMethod(objc_getMetaClass("CJPerson"), @selector(say6661));
        const char *type = method_getTypeEncoding(say6661);
        return class_addMethod(objc_getMetaClass("CJPerson"), sel, imp, type);
    }
    return NO;
}
@end
  • 结论:

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

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

三、消息转发

通过instrumentObjcMessageSends方式打印发送消息的日志。在慢速方法查找的插入缓存流程中:log_and_fill_cache -> logMessageSend,找到了instrumentObjcMessageSends源码实现。如果要打印消息发送运行日志,首先需要控制objcMsgLogEnabledtrue,同时能够在发送消息的地方调用instrumentObjcMessageSends方法才行。

  • 根据以上两点,完成下面的案例:
#import <Foundation/Foundation.h>
#import <objc/message.h>

@interface CJPerson: NSObject
- (void)sayHello;
@end

@implementation CJPerson
// 屏蔽复写resolveInstanceMethod方法与resolveClassMethod方法
// 否则打印里面会多其他对象如NSString类型的方法流程
// 就这样子简洁使用
@end
extern bool instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
    @autoreleseaPool {
        CJPerson * person = [CJPerson alloc];
        instrumentObjcMessageSends(YES);
        [person sayHello];
        instrumentObjcMessageSends(NO);
    }
    return 0;
}

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

  • 图:

截屏2023-01-12 下午4.44.12.png

  • 打开日志发现,在崩溃前调用了如下方法:

截屏2023-01-12 下午4.48.55.png

  • 结论:

打印了慢速查找后的整个流程是先动态方法决议,再快速消息转发慢速消息转发后面第二次动态决议,最后报错。

  1. 动态方法决议:resolveInstanceMethod
  2. 快速消息转发:forwardingTargetForSelector
  3. 慢速消息转发:methodSignatureForSelector

快速消息转发forwardingTargetForSelector

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

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

// 快速消息转发
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"给你二次机会...%@", NSStringFromSelector(aSelector));
    return [super forwardingTargetForSelector:aSelector];
}
  • 结论:
  1. 运行依然崩溃。因为依然没有找到方法实现。但是在快速消息转发中的打印了:给你二次机会...sayHello。也就是说在错过第一次补救机会动态方法决议后,快速消息转发forwardingTargetForSelector会给我们第二次的补救机会。

使用快速消息转发

这次机会我们可以理解为甩锅,简单理解为:我处理不了了,你让别人帮我处理吧!

  1. 现在对上面的案例进行一些修改,添加一个CJAnimal,声明并实现sayHello方法
@interface CJAnimal : NSObject
- (void)sayHello;
@end

@implementation CJAnimal
- (void)sayHello {
    NSLog(@"sayHello %s", __func__ );
}
@end
  1. CJPerson类的快速消息转发中,将方法甩锅给CJAnimal对象。也就是抓住这次机会,重新设置一个方法接受者
// 快速方法转发
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"给你二次机会...%@", NSStringFromSelector(aSelector));
    return [CJAnimal alloc];
}
  • 结论:

    ①. 运行后不再崩溃,并成功调用了CJAnimal中的sayHello方法。说明这次甩锅起到了效果,将方法的接受者变成了CJAnimal对象

    ②. 需要说明的是,在CJAnimal中寻找sayHello方法时,依然会走快速查找慢速查找动态方法决议等流程。

慢速消息转发methodSignatureForSelector

根据上面的流程我们知道,慢速消息转发流程调用了methodSignatureForSelector方法。

  • 在苹果官方文档中搜索methodSignatureForSelector方法的使用说明,发现需要配合invocation使用,即需要实现forwardInvocation方法。见下图:

12975268-cb8139c231d1be88.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@:"];
}

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

2023-01-12 17:41:58.909171+0800 testOD1[87290:19787932] 给你一次机会...
2023-01-12 17:42:00.759684+0800 testOD1[87290:19787932] 给你二次机会...sayHello
2023-01-12 17:42:01.438143+0800 testOD1[87290:19787932] 给你第三次机会...sayHello
2023-01-12 17:42:01.439236+0800 testOD1[87290:19787932] 给你一次机会...
Program ended with exit code: 0

程序运行到此处,可以理解为:爱谁处理,谁处理,反正我是不处理了。在快速消息转发中,只可修改方法的接受者;而在慢速消息转发中可以重新设置方法、接受者等,更加灵活,权限更大

使用慢速消息转发

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

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"给你第三次机会...%@", NSStringFromSelector(aSelector));
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"forwardInvocation %s", __func__ );

    CJAnimal *animal = [CJAnimal alloc]; // self;
    anInvocation.target = animal;
    anInvocation.selector = @selector(sayHello); // @selector(sayHello1);
    [anInvocation invoke];
}
  • 结论:

结果如下。不崩溃,并且从一开始的调用CJPerson对象的sayHello,到成功调用CJAnimal对象的sayHello方法。

2023-01-12 17:49:53.173226+0800 testOD1[87576:19797552] 给你一次机会...
2023-01-12 17:49:54.700033+0800 testOD1[87576:19797552] 给你二次机会...sayHello
2023-01-12 17:49:55.300064+0800 testOD1[87576:19797552] 给你第三次机会...sayHello
2023-01-12 17:49:55.300729+0800 testOD1[87576:19797552] 给你一次机会...
2023-01-12 17:49:56.781131+0800 testOD1[87576:19797552] forwardInvocation -[CJPerson forwardInvocation:]
2023-01-12 17:49:56.781496+0800 testOD1[87576:19797552] sayHello -[CJAnimal sayHello]
Program ended with exit code: 0
  • 补充:

在打印结果中发现,第二次动态方法决议在 methodSignatureForSelector  forwardInvocation方法之间。

四、总结

objc_msgSend消息慢速查找后的动态方法决议与消息转发流程就已经验证完了:先进行第一次动态方法决议,没有进行消息快速转发,再没有进行消息慢速转发,在消息慢速转发中进行第二次动态方法决议

  • 基本流程就如下图:

截屏2023-01-08 上午3.05.20.png

五、补充点:

objc_msgSend发送消息的流程

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

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

oopaop

oop:面向对象编程,什么人做什么什么事情,分工非常明确。

  • 好处:耦合度很低
  • 痛点:有很多冗余代码,常规解决办法是提取,那么会有一个公共的类,所有人对公共的类进行集成,那么所有人对公共类进行强依赖,也就代表着出现了强耦合

aop:面向切面编程,是oop的延伸

  • 切点:要切入的方法和切入的类,比如上述的例子中的sayHello和CJPerson
  • 优点:对业务无侵入,通过动态方式将某些方法进行注入
  • 缺点:做了一些判断,执行了很多无关代码,包括系统方法,造成性能消耗。会打断apple的方法动态转发流程。