在前一篇文章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是什么呢?
- 在慢速查找流程的
lookUpImpOrForward函数的第一行代码,即对forward_imp进行了赋值:
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
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-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);
}
- 结论:
- 此函数里面有三个关键的函数:
- ①
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_locked与resolveInstanceMethod函数都会执行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指令在终端打印内存情况!
- 结论:
-
第一次动态决议:第一次,和我们分析的是一致的,是在查找
sayHello方法时没有找到,会进入动态方法决议,发送resolveInstanceMethod消息。 -
第二次动态决议:第二次,是在调用了
CoreFoundation框架中的NSObject(NSObject) methodSignatureForSelector:后,会再次进入动态决议。
探索resolveInstanceMethod动态方法决议流程
虽然重写了动态方法决议方法resolveInstanceMethod,但是依然报错,并且该方法还被调用了两次。下面进行代码跟踪调试。
- 再次运行上面的案例,过滤出我们需要研究的内容,即
CJPerson对象调用sayHello方法,第一次进入动态方法决议方法resolveInstanceMethod。
- 图:
- 判断
cls,也就是CJPerson,是否实现了resolveInstanceMethod类方法。
- 图:
- 在元类中进行方法查找(即查找类方法),是否实现了类方法
resolveInstanceMethod,路径为:lookUpImpOrNilTryCache -> _lookUpImpTryCache -> lookUpImpOrForward
- 图:
- 在方法列表
methods中,成功找到resolveInstanceMethod方法,并插入缓存。
- 图:
- 如果没有找到,此处会直接返回,即没有利用这次机会,直接返回!而如果找到则发送一条
resolveInstanceMethod消息,即执行resolveInstanceMethod方法。
- 图:
- 完成消息发送后,会再进行
sayHello方法的查找,但是依然找不到!因为CJPerson虽然实现了resolveInstanceMethod,但是里面什么也没有做!
- 图:
resolveInstanceMethod执行完成后,回到resolveMethod_locked流程中,调用lookUpImpOrForwardTryCache再次进行方法查找。
- 图:
- 在
lookUpImpOrForwardTryCache中,依然没有查找到sayHello,此时会从缓存中返回forward_imp。
- 图:
- 最后动态方法决议流程结束,只是此案例中,虽然实现了
动态方法决议,但是里面什么也没有做,进入消息转发,最终运行结果报错!
- 图:
- 结论:
通过上面的案例,理清楚了动态方法决议的流程。但是实际开发过程中,我们肯定会抓住这次处理错误的机会。下面我们对案例进行修改,将sayHello方法指向其他方法。
使用resolveInstanceMethod动态方法决议
依然是上面的案例,如果我们向类中添加一个方法,方法的sel依然是sayHello,但是其对应的方法实现imp为sayHello1的实现。
// 动态方法决议 - 给一次机会, 需要进行方法实现
+ (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函数改成了只适配arm64e的bigSigned();由于调试源码是macOS环境,将bigSigned()改回big();如果是iOS真机不需要更改。
使用动态决议流程探索
运行复写类方法resolveInstanceMethod方法的代码,这次有什么不同呢?我们依然把关注点放到resolveInstanceMethod方法中,跟踪①、②、③三个地方分别做了什么?
调用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 - 进入
lookUpImpOrForward,查找sayHello方法 - 最终在方法列表中找到了,并将其插入缓存中。
- 继续运行代码,回到
resolveMethod_locked,并再次调用lookUpImpOrForwardTryCache方法,进行方法查找。 - 此时,通过
cache_getImp找到了方法实现,方法实现为sayHello1,返回imp。
- ③调用路径:
类方法的动态决议
对于类方法的动态决议,与实例方法类似;同样可以通过重写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源码实现。如果要打印消息发送运行日志,首先需要控制objcMsgLogEnabled为true,同时能够在发送消息的地方调用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/,就可以找到运行日志。
- 图:
- 打开日志发现,在崩溃前调用了如下方法:
- 结论:
打印了慢速查找后的整个流程是先动态方法决议,再快速消息转发与慢速消息转发后面第二次动态决议,最后报错。
- 动态方法决议:
resolveInstanceMethod - 快速消息转发:
forwardingTargetForSelector - 慢速消息转发:
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];
}
- 结论:
- 运行依然崩溃。因为依然没有找到方法实现。但是在快速消息转发中的打印了:
给你二次机会...sayHello。也就是说在错过第一次补救机会动态方法决议后,快速消息转发forwardingTargetForSelector会给我们第二次的补救机会。
使用快速消息转发
这次机会我们可以理解为甩锅,简单理解为:我处理不了了,你让别人帮我处理吧!
- 现在对上面的案例进行一些修改,添加一个
CJAnimal类,声明并实现sayHello方法。
@interface CJAnimal : NSObject
- (void)sayHello;
@end
@implementation CJAnimal
- (void)sayHello {
NSLog(@"sayHello %s", __func__ );
}
@end
CJPerson类的快速消息转发中,将方法甩锅给CJAnimal对象。也就是抓住这次机会,重新设置一个方法接受者。
// 快速方法转发
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"给你二次机会...%@", NSStringFromSelector(aSelector));
return [CJAnimal alloc];
}
-
结论:
①. 运行后不再崩溃,并成功调用了
CJAnimal中的sayHello方法。说明这次甩锅起到了效果,将方法的接受者变成了CJAnimal对象。②. 需要说明的是,在
CJAnimal中寻找sayHello方法时,依然会走快速查找、慢速查找、动态方法决议等流程。
慢速消息转发methodSignatureForSelector
根据上面的流程我们知道,慢速消息转发流程调用了methodSignatureForSelector方法。
- 在苹果官方文档中搜索
methodSignatureForSelector方法的使用说明,发现需要配合invocation使用,即需要实现forwardInvocation方法。见下图:
继续上面的案例,不在快速消息转发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消息慢速查找后的动态方法决议与消息转发流程就已经验证完了:先进行第一次动态方法决议,没有进行消息快速转发,再没有进行消息慢速转发,在消息慢速转发中进行第二次动态方法决议。
- 基本流程就如下图:
五、补充点:
objc_msgSend发送消息的流程
到目前为止,objc_msgSend发送消息的流程就分析完成了,在这里简单总结下:
- 快速查找流程:在类的缓存
cache中查找指定方法的实现。 - 慢速查找流程:如果缓存中没有找到,则在类的方法列表
methods中查找,如果还是没找到,则去父类链的缓存和方法列表中查找。 - 动态方法决议:如果慢速查找还是没有找到时,第一次补救机会就是尝试一次动态方法决议,即实例方法重写
resolveInstanceMethod/类方法重写resolveClassMethod方法。 - 消息转发:如果动态方法决议还是没有找到,则进行消息转发,消息转发中有两次补救机会:快速转发
forwardingTargetForSelector、慢速转发methodSignatureForSelector。 - 报错崩溃:如果消息转发之后也没有,则程序直接报错崩溃
unrecognized selector sent to instance。
oop与aop
oop:面向对象编程,什么人做什么什么事情,分工非常明确。
- 好处:耦合度很低
- 痛点:有很多冗余代码,常规解决办法是提取,那么会有一个公共的类,所有人对公共的类进行集成,那么所有人对公共类进行强依赖,也就代表着出现了强耦合
aop:面向切面编程,是oop的延伸
- 切点:要切入的方法和切入的类,比如上述的例子中的sayHello和CJPerson
- 优点:对业务无侵入,通过动态方式将某些方法进行注入
- 缺点:做了一些判断,执行了很多无关代码,包括系统方法,造成性能消耗。会打断apple的方法动态转发流程。