在前两篇文章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:后,会再次进入动态决议。
5.消息转发探索
从上面的流程中了解到,objc_msgSend在完成快速查找和慢速查找后,均没有找到方法,就会进行动态方法决议,如果动态方法决议也没有处理,则会进行消息转发流程。但是,我们找遍了也没有发现消息转发的相关源码。
通过instrumentObjcMessageSends方式打印发送消息的日志。在慢速方法查找的插入缓存流程中:log_and_fill_cache -> logMessageSend,找到了instrumentObjcMessageSends源码实现。如果要打印消息发送运行日志,首先需要控制objcMsgLogEnabled为true,同时能够在发送消息的地方调用instrumentObjcMessageSends方法才行。根据以上两点,完成下面的案例:
extern bool instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
Class pClass = [LGPerson class];
LGPerson * lg = [LGPerson alloc];
instrumentObjcMessageSends(YES);
[lg sayHello];
instrumentObjcMessageSends(NO);
}
return 0;
}
日志打印在哪里呢?在logMessageSend 源码实现中,已经告诉我们了。
snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
运行以上代码,然后Finder中前往/tmp/msgSends/,就可以找到运行日志,见下图:
打开日志发现,在崩溃前调用了如下方法:
- 动态方法决议:
resolveInstanceMethod - 快速消息转发:
forwardingTargetForSelector - 慢速消息转发:
methodSignatureForSelector
当然我们还可以通过hopper/IDA反编译来探究消息转发流程。在之后的篇幅中再详细介绍。
6.快速消息转发
通过日志我们了解到forwardingTargetForSelector对象方法实际调用者是LGPerson对象,所以在LGPerson类中添加对象方法的forwardingTargetForSelector实现,依然调用LGPerson的对象方法sayHello。
// 动态方法决议 - 给一次机会, 需要进行方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"给你第一次机会...");
return [super resolveInstanceMethod:sel];
}
// 快速消息转发
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"给你二次机会...%@", NSStringFromSelector(aSelector));
return [super forwardingTargetForSelector:aSelector];
}
运行结果发现,依然崩溃,因为依然没有找到方法实现。但是在快速消息转发中的打印结果为:给你二次机会...sayHello。也就是说在错过第一次补救机会动态方法决议后,快速消息转发forwardingTargetForSelector会给我们第二次的补救机会。
这次机会我们可以理解为甩锅,简单理解为:我处理不了了,你让别人帮我处理吧!
现在对上面的案例进行一些修改,添加一个LGHuman类,声明并实现sayHello方法。
@interface LGHuman : NSObject
-(void)sayHello;
@end
@implementation LGHuman
-(void)sayHello {
NSLog(@"sayHello %s", __func__);
}
@end
LGPerson类的快速消息转发中,将方法甩锅给LGHuman对象。也就是抓住这次机会,重新设置一个方法接受者。
// 给第二次机会,重新设置接受者
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"给你二次机会...%@", NSStringFromSelector(aSelector));
return [LGHuman alloc];
// return [super forwardingTargetForSelector:aSelector];
}
运行后不再崩溃,并成功调用了LGHuman中的sayHello方法。说明这次甩锅起到了效果,将方法的接受者变成了LGHuman对象。需要说明的是,在LGHuman中寻找sayHello方法时,依然会走快速查找、慢速查找、动态方法决议等流程。
7.慢速消息转发
根据上面的流程我们知道,慢速消息转发流程调用了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@:"];
// return [super methodSignatureForSelector:aSelector];
}
打印结果发现,forwardInvocation方法中即使不对invocation事务进行处理,也不会崩溃报错了。
程序运行到此处,可以理解为:爱谁处理,谁处理,反正我是不处理了。在快速消息转发中,只可修改方法的接受者;而在慢速消息转发中可以重新设置方法、接受者等,更加灵活,权限更大。
在方法forwardInvocation方法中做一些修改,重新设置事务的target = LGHuman、selector = sayHello8。
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"forwardInvocation %s", __func__);
anInvocation.target = [LGHuman alloc];
anInvocation.selector = @selector(sayHello8);
[anInvocation invoke];
}
运行结果见下图。不崩溃,并且从一开始的调用LGPerson对象的sayHello,到成功调用LGHuman对象的sayHello8方法。
补充:在打印结果中发现,第二次动态方法决议在 methodSignatureForSelector 和 forwardInvocation方法之间。
示例完整代码:
@interface LGPerson : NSObject
-(void)sayHello;
@end
#import "LGPerson.h"
#import "LGHuman.h"
@implementation LGPerson
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"给你一次机会...");
return [super resolveInstanceMethod:sel];
}
-(id) forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"给你二次机会...%s", sel_getName(aSelector));
return [super forwardingTargetForSelector:aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"给你三次机会...%s", sel_getName(aSelector));
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"forwardInvocation %s", __func__);
anInvocation.target = [LGHuman alloc];
anInvocation.selector = @selector(sayHello8);
[anInvocation invoke];
}
@end
@interface LGHuman : NSObject
-(void)sayHello8;
@end
@implementation LGHuman
- (void)sayHello8{
NSLog(@"sayHello8 %s", __func__);
}
@end
8.总结
到目前为止,objc_msgSend发送消息的流程就分析完成了,在这里简单总结下。
-
快速查找流程:首先,在类的缓存cache中查找指定方法的实现。 -
慢速查找流程:如果缓存中没有找到,则在类的方法列表中查找,如果还是没找到,则去父类链的缓存和方法列表中查找。 -
动态方法决议:如果慢速查找还是没有找到时,第一次补救机会就是尝试一次动态方法决议,即重写resolveInstanceMethod/resolveClassMethod 方法。 -
消息转发:如果动态方法决议还是没有找到,则进行消息转发,消息转发中有两次补救机会:快速转发、慢速转发。 -
如果转发之后也没有,则程序直接报错崩溃
unrecognized selector sent to instance。