在前两篇文章objc_msgSend快速方法查找和objc_msgSend慢速方法查找中,探究了函数调用的本质,即消息发送:objc_msgSend
,并跟踪源码学习了方法查找的流程。本篇关注如果快速查找和慢速查找都没有找到方法怎么呢?
1.问题解析
根据前两篇文章,提出两个问题:
forward_imp
是什么?- 如果方法找不到,如何补救?
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);
}
复制代码
流程分析:
- 判断
cls
的类型; - 如果是类,则执行
实例方法
的动态决议resolveInstanceMethod
方法。 - 如果是元类,则执行
类方法
的动态决议resolveClassMethod
方法。如果元类中没有找到该实例方法或者为空,则在元类的实例方法的动态方法决议resolveInstanceMethod
中查找。为什么呢?因为类方法在元类中,是以对象方法的形式存储,所以需要执行元类的实例对象决议方法。也就是说类是元类的实例对象
。 - 如果方法决议将方法的实现指向了其他地方,则继续执行最后一行的
lookUpImpOrForwardTryCache
方法,进行方法查找流程,并返回imp
。
2.resolveInstanceMethod源码分析
对象方法动态方法决议会调用resolveInstanceMethod
方法。源码如下:
流程解析:
A处
:进行慢速方法查找,判断类是否实现了resolveInstanceMethod
方法;如果没有找到,直接返回;B处
:如果找到,则发送消息,执行resolveInstanceMethod
方法;C处
:再次进行方法查找,即通过_lookUpImpTryCache
方法进入lookUpImpOrForward
进行慢速方法查找。
连续多次的方法查找,很混乱,每一步都做了什么呢?下面用案例进行探索分析。
3.案例初步探索
在LGPerson类
的声明中添加两个方法,-(void)sayHello;
和-(void)sayHello1;
;类实现中,重写resolveInstanceMethod
方法,并实现方法-(void)sayHello1
,而-(void)sayHello
并未实现。
@interface LGPerson : NSObject
-(void)sayHello;
-(void)sayHello1;
+ (void)say666;
+ (void)say6661;
@end
@implementation LGPerson
- (void)sayHello1{
NSLog(@"sayHello1 %s", __func__);
}
+ (void)say6661{
NSLog(@"say6661 %s", __func__);
}
// 动态方法决议 - 给一次机会, 需要进行方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"给你一次机会...");
// 什么也没做
return 0;
}
@end
复制代码
案例运行结果:
案例解析:
虽然重写了动态方法决议方法resolveInstanceMethod
,但是依然报错,并且该方法还被调用了两次
。为什么呢?下面进行代码跟踪调试。
-
再次运行上面的案例,过滤出我们需要研究的内容,即
LGPerson
对象调用sayHello
方法,进入动态方法决议方法resolveInstanceMethod
。见下图: -
判断
cls
,也就是LGPerson
,是否实现了resolveInstanceMethod
类方法。见下图: -
在元类中进行方法查找,是否实现了
resolveInstanceMethod
,路径为:lookUpImpOrNilTryCache -> _lookUpImpTryCache -> lookUpImpOrForward
,见下图: -
在方法列表中,成功找到
resolveInstanceMethod
方法,并插入缓存。 -
如果没有找到,此处会直接返回,即
没有利用这次机会
,直接返回!而如果找到则发送一条resolveInstanceMethod消息
,即执行resolveInstanceMethod方法
。 -
完成消息发送后,会再进行
sayHello
方法的查找,但是依然找不到!因为LGPerson
虽然实现了resolveInstanceMethod
,但是里面什么也没有做
! -
resolveInstanceMethod
执行完成后,回到resolveMethod_locked
流程中,调用lookUpImpOrForwardTryCache
再次进行方法查找。 -
在
lookUpImpOrForwardTryCache
中,依然查找sayHello
,此时会从缓存中返回forward_imp
,也就是进行消息转发
。 -
动态方法决议流程结束,只是此案例中,虽然实现了
动态方法决议
,但是里面什么也没有做,进入消息转发
,最终运行结果报错!
总结
:通过上面的案例,理清楚了动态方法决议的流程。但是实际开发过程中,我们肯定会抓出这次处理错误的机会。下面我们对案例进行修改,将sayHello
方法指向其他方法。
4.案例深入探索
依然是上面的案例,但是我们抓住这次机会,向类中添加一个方法,方法的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));
class_addMethod(self, sel, imp, method_getTypeEncoding(method));
return NO;
}
return [super resolveInstanceMethod:sel];
}
复制代码
运行上面的代码,这次有什么不同呢?我们依然把关注点放到resolveInstanceMethod方法
中,跟踪A
、B
、C
三个地方分别作了什么?
-
调用
LGPerson类
的实例方法sayHello
,分别进行快速查找和慢速查找,均找不到该方法。最终会进入源码的resolveMethod_locked
->resolveInstanceMethod
流程中。 -
代码运行到
A处
,会查找类是否实现了resolveInstanceMethod
,如果实现了,则会将该方法插入缓存,以便下次进行快速方法查找;如果没有实现,直接返回。此处流程和初探时的流程是一致的! -
代码运行到
B处
,发送msg
,即执行LGPerson 类
中的resolveInstanceMethod
方法。由于将sayHello
方法指向了sayHello1
,则此处class_addMethod
会将方法插入class_rw_ext_t
,也就是插入LGPerson
的方法列表中;方法插入流程,见下图:
至此,动态方法决议方法已经执行一次,并重新设定了方法实现。
-
代码运行到
C处
,再次查找sayHello
,此流程会在方法列表中查找到方法实现sayHello1
,并以sel=sayHello
,imp=sayHello1实现
的形式插入方法缓存。C处
调用路径:lookUpImpOrNilTryCache -> _lookUpImpTryCache -> lookUpImpOrForward
。
- 进入
lookUpImpOrForward
,查找sayHello
方法。
最终在方法列表中找到了,并将其插入缓存中。
-
继续运行代码,回到
resolveMethod_locked
,并再次调用lookUpImpOrForwardTryCache
方法,进行方法查找。此时,通过
cache_getImp
找到了方法实现,方法实现为sayHello1
,返回imp
。
疑问说明:
resolveInstanceMethod
返回NO
,才接着走后面的转发流程,而返回YES
就停止转发了?
- 其实如果重写的
resolveInstanceMethod
什么也不做,只是返回YES
也会接着走后面的转发流程。这个返回值对于消息转发流程没有任何意义,从runtime
的源码来看这个返回值只和debug
的信息相关。
5.类方法动态决议
针对类方法,与实例方法类似,同样可以通过重写resolveClassMethod类方法
来解决前文的崩溃问题,即在LGPerson类
中重写该方法,并将say666
类方法的实现指向类方法say6661
。
+(BOOL)resolveClassMethod:(SEL)sel{
NSLog(@"给你一次机会...+++");
if (sel == @selector(say666))
{
Class meteCls = objc_getMetaClass("LGPerson");
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];
}
复制代码
使用说明:
resolveClassMethod类方法
的重写需要注意一点,传入的cls
不再是类,而是元类
,可以通过objc_getMetaClass方法
获取类的元类,原因是因为类方法在元类中是实例方法。
3.动态方法决议使用优化
上面的这种方式是单独在每个类中重写,有没有更好的,一劳永逸的方法呢?其实通过方法慢速查找流程可以发现其查找路径有两条:
实例方法
:类 -- 父类 -- 根类 -- nil
类方法
:元类 -- 根元类 -- 根类 -- nil
它们的共同点是如果前面没找到,都会来到根类
即NSObject
中查找,所以我们是否可以将上述的两个方法统一整合在一起呢?答案是可以的,可以通过NSObject
添加分类的方式来实现统一处理,而且由于类方法的查找,在其继承链,查找的也是实例方法,所以可以将实例方法
和类方法
的统一处理放在resolveInstanceMethod方法
中,如下所示:
// NSObject分类
@implementation NSObject (GF)
+ (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("LGPerson"), @selector(say6661));
Method lgClassMethod = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(say6661));
const char *type = method_getTypeEncoding(say6661);
return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
}
return NO;
}
@end
复制代码
这种方式的实现,正好与源码中针对类方法的处理逻辑是一致的,即完美阐述为什么调用了类方法动态方法决议,还要调用对象方法动态方法决议,其根本原因还是类方法在元类中的实例方法
。
当然,上面这种写法还是会有其他的问题,比如系统方法也会被更改,针对这一点,是可以优化的,即我们可以针对自定义类中方法统一方法名的前缀
,根据前缀
来判断是否是自定义方法,然后统一处理自定义方法,例如可以在崩溃前pop
到首页,主要是用于app
线上防崩溃的处理,提升用户的体验。
4.动态方法决议执行两次探索
以对象方法决议resolveInstanceMethod
为例,我们可以写个示例测试一下,调用一个未实现的SEL
,并重写resolveInstanceMethod
,但是不对方法进行重定向,然而发现,这个方法竟然被调用了2次
。
见下面的示例截图:
从上面的案例结果中可以发现,resolveInstanceMethod
动态决议方法中给你一次机会...
打印了两次,这是为什么呢?
通过bt
查看堆栈信息可以看出:
-
第一次动态决议:
第一次,和我们分析的是一致的,是在查找sayHello方法
时没有找到,会进入动态方法决议,发送resolveInstanceMethod消息
。 -
第二次动态决议:
第二次,是在调用了CoreFoundation框架
中的NSObject(NSObject) methodSignatureForSelector:
后,会再次进入动态决议。