在前一篇文章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指令在终端打印内存情况!

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

  • 定论:
  1. 榜首次动态抉择:榜首次,和咱们分析的是共同的,是在查找sayHello办法时没有找到,会进入动态办法抉择,发送resolveInstanceMethod音讯。

  2. 第2次动态抉择:第2次,是在调用了CoreFoundation结构中的NSObject(NSObject) methodSignatureForSelector:后,会再次进入动态抉择

探究resolveInstanceMethod动态办法抉择流程

尽管重写了动态办法抉择办法resolveInstanceMethod,可是仍然报错,而且该办法还被调用了两次。下面进行代码盯梢调试。

  1. 再次运转上面的事例,过滤出咱们需求研究的内容,即CJPerson目标调用sayHello办法,榜首次进入动态办法抉择办法resolveInstanceMethod
  • 图:
    OC底层原理(十):objc_msgSend的动态方法决议与消息转发
  1. 判别cls,也便是CJPerson,是否完结了resolveInstanceMethod类办法
  • 图:
    OC底层原理(十):objc_msgSend的动态方法决议与消息转发
  1. 元类中进行办法查找(即查找类办法),是否完结了类办法resolveInstanceMethod,途径为:lookUpImpOrNilTryCache -> _lookUpImpTryCache -> lookUpImpOrForward
  • 图:
    OC底层原理(十):objc_msgSend的动态方法决议与消息转发
  1. 办法列表methods中,成功找到resolveInstanceMethod办法,并刺进缓存
  • 图:
    OC底层原理(十):objc_msgSend的动态方法决议与消息转发
  1. 假如没有找到,此处会直接回来,即没有运用这次时机,直接回来!而假如找到则发送一条resolveInstanceMethod音讯,即履行resolveInstanceMethod办法。
  • 图:
    OC底层原理(十):objc_msgSend的动态方法决议与消息转发
  1. 完结音讯发送后,会再进行sayHello办法的查找,可是仍然找不到!因为CJPerson尽管完结了resolveInstanceMethod,可是里边什么也没有做
  • 图:
    OC底层原理(十):objc_msgSend的动态方法决议与消息转发
  1. resolveInstanceMethod履行完结后,回到resolveMethod_locked流程中,调用lookUpImpOrForwardTryCache再次进行办法查找
  • 图:
    OC底层原理(十):objc_msgSend的动态方法决议与消息转发
  1. lookUpImpOrForwardTryCache中,仍然没有查找到sayHello,此刻会从缓存中回来forward_imp
  • 图:
    OC底层原理(十):objc_msgSend的动态方法决议与消息转发
  1. 最终动态办法抉择流程完毕,只是此事例中,尽管完结了动态办法抉择,可是里边什么也没有做,进入音讯转发,终究运转成果报错!
  • 图:
    OC底层原理(十):objc_msgSend的动态方法决议与消息转发

  • 定论:

经过上面的事例,理清楚了动态办法抉择的流程。可是实践开发进程中,咱们肯定会捉住这次处理过错的时机。下面咱们对事例进行修正,将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真机不需求更改。

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

运用动态抉择流程探究

运转复写类办法resolveInstanceMethod办法的代码,这次有什么不同呢?咱们仍然把重视点放到resolveInstanceMethod办法中,盯梢①、②、③三个当地别离做了什么?

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

调用CJPerson类的实例办法sayHello,别离进行快速查找慢速查找,均找不到该办法。终究会进入源码动态办法抉择resolveMethod_locked -> resolveInstanceMethod流程中:

  • 运转到①、会查找类是否完结了resolveInstanceMethod?假如完结了,则会将该办法刺进缓存,以便下次进行快速办法查找;假如没有完结,直接回来。此处流程和初探时的流程是共同的!

  • 运转到②,发送msg,即履行CJPerson 类中的resolveInstanceMethod办法。因为将sayHello办法指向了sayHello1,则此处class_addMethod会将办法刺进class_rw_ext_t,也便是刺进CJPerson办法列表中。

  • 运转到③,再次查找sayHello,此流程会在办法列表中查找到办法完结sayHello1,并以sel=sayHelloimp=sayHello1完结的形式刺进办法缓存。

    • ③调用途径:lookUpImpOrNilTryCache -> _lookUpImpTryCache -> lookUpImpOrForward
      OC底层原理(十):objc_msgSend的动态方法决议与消息转发
    • 进入lookUpImpOrForward,查找sayHello办法
      OC底层原理(十):objc_msgSend的动态方法决议与消息转发
    • 终究在办法列表中找到了,并将其刺进缓存中。
    • 继续运转代码,回到resolveMethod_locked,并再次调用lookUpImpOrForwardTryCache办法,进行办法查找
      OC底层原理(十):objc_msgSend的动态方法决议与消息转发
    • 此刻,经过cache_getImp找到了办法完结,办法完结为sayHello1,回来imp
      OC底层原理(十):objc_msgSend的动态方法决议与消息转发

类办法的动态抉择

对于类办法的动态抉择,与实例办法类似;同样能够经过重写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/,就能够找到运转日志。

  • 图:

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

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

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

  • 定论:

打印了慢速查找后的整个流程是先动态办法抉择,再快速音讯转发慢速音讯转发后边第2次动态抉择,最终报错。

  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会给咱们第2次的弥补时机。

运用快速音讯转发

这次时机咱们能够理解为甩锅,简单理解为:我处理不了了,你让别人帮我处理吧!

  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办法。见下图:

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

继续上面的事例,不在快速音讯转发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
  • 弥补:

在打印成果中发现,第2次动态办法抉择methodSignatureForSelectorforwardInvocation办法之间。

四、总结

objc_msgSend音讯慢速查找后的动态办法抉择与音讯转发流程就现已验证完了:先进行榜首次动态办法抉择,没有进行音讯快速转发,再没有进行音讯慢速转发,在音讯慢速转发中进行第2次动态办法抉择

  • 基本流程就如下图:

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

五、弥补点:

objc_msgSend发送音讯的流程

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

  • 快速查找流程:在类的缓存cache中查找指定办法的完结。
  • 慢速查找流程:假如缓存没有找到,则在类的办法列表methods中查找,假如找到,则去父类链缓存和办法列表中查
  • 动态办法抉择:假如慢速查找仍是没有找届时,榜首次弥补时机便是尝试一次动态办法抉择,即实例办法重写resolveInstanceMethod/类办法重写resolveClassMethod办法。
  • 音讯转发:假如动态办法抉择仍是没有找到,进行音讯转发,音讯转发中有两次弥补时机:快速转发forwardingTargetForSelector慢速转发methodSignatureForSelector
  • 报错溃散:假如音讯转发之后也没有,则程序直接报错溃散unrecognized selector sent to instance

oopaop

oop:面向目标编程,什么人做什么什么事情,分工十分清晰。

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

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

  • 切点:要切入的办法和切入的类,比如上述的比如中的sayHello和CJPerson
  • 优点:对事务无侵入,经过动态办法将某些办法进行注入
  • 缺点:做了一些判别,履行了许多无关代码,包括体系办法,形成功能消耗。会打断apple的办法动态转发流程。