前言:前面分析过了Runtime的三个阶段中的查找方法的过程,分为了慢速查找和快速查找,在分析的时候我们还发现如果查找结束没有发现方法的时候,系统并没有直接返回失败的信息,而是中间又经历了其他的容错处理,增加了程序的健壮性和扩展性,为开发者提供了更大的操作空间!接下来就让我们继续看看具体系统做了些什么?
runtime消息发送系列文章友情链接
一、动态方法解析
1、 我们通过方法查找部分的代码,可以得到如下的源码:
// No implementation found. Try method resolver once. 011 010
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
上述方法为单例实现,表示未找到方法实现的话,就进入动态方法决议阶段
2、跟踪方法解析过程
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
#pragma mark - 如果不是类方法,就进入实例化方法处理
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else {
#pragma mark -如果是类方法,就进入类方法处理
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
#pragma mark - 进入缓存查找
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
看上面传入的是类方法还是实例方法去走不同的逻辑,接下来以实例方法为例,进行跟踪
3、动态方法决议过程(实例化方法为例)
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
#pragma mark - 如果没有进行动态方法解析,中断执行后续
if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
// Resolver not implemented.
return;
}
#pragma mark - 进行了动态方法解析后,这里进行消息发送操作,执行objc_msgSend
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
//这里进行了缓存方法操作,下次就不走这里了
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
......
//打印日志的一些操作省略......
}
}
这里是对动态决议过程的结果处理,如果动态决议对方法进行了处理,这里重新执行objc_msgSend,也是为什么自定义类中的 + resolveInstanceMethod 方法执行了两次的原因。到这里动态决议结束,如果没有动态方法决议处理操作,就继续进行下一步,我们继续跟踪源码。
4、动态方法决议未实现的后续流程分析
为了知道程序在进行了动态方法决议之后的执行流程,我们新建一个demo工程进行分析。
我们在方法动态解析的地方添加相应的方法other,可以看到程序运行的结果:
2021-07-04 20:50:59.009403+0800 方法调用demo[1611:5853209] -[Person other]
Program ended with exit code: 0
我们不为程序添加动态解析,得到程序崩溃信息,使用lldb命令中的bt命令,得到堆栈信息:
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGABRT
frame #0: 0x00007fff2043f92e libsystem_kernel.dylib`__pthread_kill + 10
frame #1: 0x000000010038de79 libsystem_pthread.dylib`pthread_kill + 263
frame #2: 0x00007fff203c3411 libsystem_c.dylib`abort + 120
frame #3: 0x00007fff20431ef2 libc++abi.dylib`abort_message + 241
frame #4: 0x00007fff204235fd libc++abi.dylib`demangling_terminate_handler() + 266
frame #5: 0x00007fff2031c58d libobjc.A.dylib`_objc_terminate() + 96
frame #6: 0x00007fff20431307 libc++abi.dylib`std::__terminate(void (*)()) + 8
frame #7: 0x00007fff20433beb libc++abi.dylib`__cxxabiv1::failed_throw(__cxxabiv1::__cxa_exception*) + 27
frame #8: 0x00007fff20433bb2 libc++abi.dylib`__cxa_throw + 116
frame #9: 0x00007fff20319ec0 libobjc.A.dylib`objc_exception_throw + 350
frame #10: 0x00007fff2066438d CoreFoundation`-[NSObject(NSObject) doesNotRecognizeSelector:] + 132
frame #11: 0x00007fff2054990b CoreFoundation`___forwarding___ + 1448
frame #12: 0x00007fff205492d8 CoreFoundation`_CF_forwarding_prep_0 + 120
* frame #13: 0x0000000100003e86 方法调用demo`main(argc=1, argv=0x00007ffeefbff468) at main.m:14:9
frame #14: 0x00007fff20489f5d libdyld.dylib`start + 1
frame #15: 0x00007fff20489f5d libdyld.dylib`start + 1
分析堆栈信息,从下而上的分析,我们得到 ‘CF_forwarding_prep_0’ 、 ‘__ forwarding__’ 、‘doesNotRecognizeSelector’方法。
我们通过这几个方法,可以得知,他们都是CoreFoundation库中的方法,尝试下在runtime源码中能不能找到;
最后我们通过doesNotRecognizeSelector方法找到相关源码,但是依然对分析程序没有帮助,于是我们只能通过反汇编的方式去分析CoreFoundation库(使用了工具IDA64)。
我们把搜索到的所有方法,都写入Person的实现中,然后查看方法的调用以及调用顺序,如下:
于是,我们知道了在动态消息决议之后执行的过程,下面我们就说说这样一个过程————消息转发流程!
二、消息转发
我们都知道,在执行resolveInstanceMethod没有相关处理逻辑,会进入另外一个方法:forwardingTargetForSelector,通过方法名我们知道是需要传入一个可以处理方法的对象,也就是Person类的test方法没实现,我们要传入一个其他的类去处理test方法。接下来我们就这样尝试:
1、forwardingTargetForSelector方法
这里我们发现,传入了一个实现了test方法的对象Man之后,forwardingTargetForSelector里面调用了Man的test方法,程序运行并没有崩溃,而且后面的方法都没有再走。如果我们不实现forwardingTargetForSelector,继续探索下一个方法methodSignatureForSelector。
2、methodSignatureForSelector方法
可以看到,我们在下一个方法,传入了需要的方法签名(按照声明的-(void)test;)。在执行methodSignatureForSelector方法之后,重复去调用了resolveInstanceMethod进行了消息决议,发现消息发送依然不成功,后面又去走forwardInvocation方法。
3、forwardInvocation方法
我们分析,传入了方法,不传入对象,消息发送的时候objc_msgSend就找不到消息接收者receiver,所以应该传入一个实现了test方法的类Man,我们接下来看结果:
4、doesNotRecognizeSelector方法
从调试情况分析,在各个环节都不处理的情况下,会走这个方法,同时函数会抛出异常reason: '-[xxxClass xxx]: unrecognized selector sent to instance 0x100614280'。
三、对Runtime消息发送的总结
1、forwardingTargetForSelector和methodSignatureForSelector与forwardInvocation都可以实现方法的转发过程,为什么不直接使用forwardingTargetForSelector呢?
分析和认识:虽然快速转发和慢速转发过程都能满足,而且好像forwardingTargetForSelector效率和便捷性还高点,但是为了复杂场景的需求,比如监控未实现的方法调用,崩溃的特殊处理等,在forwardInvocation中处理更方便一些!
2、消息发送的典型应用场景
比如防崩溃的处理,很多场景下,方法忘记写实现,特别是同时有很多个类的时候,我们去寻找定位,没有那么方便,通过AOP切面编程的方式,采集错误信息,可以用于日志上报等,有利于开发和维护。
3、Runtime消息发送的流程图(消息发送、动态方法决议和消息转发过程)
- 祭上我的手工流程图(消息发送和动态方法解析)
- 祭上我的手工流程图(消息转发)
以上就是关于Runtime消息发送,动态方法解析和消息转发流程的总结,后期有时间的话,还要对其做一些更新和完善,如果您喜欢的话,欢迎点赞收藏!如有问题欢迎提问,一起学习一起进步!