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方法。源码如下:
流程解析:
1:进行慢速方法查找,判断类是否实现了resolveInstanceMethod方法;如果没有找到,直接返回;2:如果找到,则发送消息,执行resolveInstanceMethod方法;3:再次进行方法查找,即通过_lookUpImpTryCache方法进入lookUpImpOrForward进行慢速方法查找。
3.案例初步探索
在JHSPerson类的声明中添加两个方法,-(void)saySomething1;和-(void)saySomething2;;类实现中,重写resolveInstanceMethod方法,并实现方法-(void)saySomething1,而-(void)saySomething2并未实现。
@interface JHSPerson : NSObject
- (void)saySomething1;
- (void)saySomething2;
@end
#import "JHSPerson.h"
@implementation JHSPerson
- (void)saySomething1{
NSLog(@"saySomething1 %s",__func__);
}
// 动态方法决议 - 给一次机会, 需要进行方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"给你一次机会...");
// 什么也没做
return 0;
}
@end
虽然重写了动态方法决议方法
resolveInstanceMethod,但是依然报错,并且该方法还被调用了两次。为什么呢?下面进行代码跟踪调试。
- 再次运行上面的案例,过滤出我们需要研究的内容,即
JHSPerson对象调用saySomething2方法,进入动态方法决议方法resolveInstanceMethod。见下图:
- 判断
cls,也就是JHSPerson,是否实现了resolveInstanceMethod类方法。见下图:
3. 在元类中进行方法查找,是否实现了
resolveInstanceMethod,路径为:lookUpImpOrNilTryCache -> _lookUpImpTryCache -> lookUpImpOrForward,见下图:
- 在方法列表中,成功找到
resolveInstanceMethod方法,并插入缓存。
- 如果没有找到,此处会直接返回,即
没有利用这次机会,直接返回!而如果找到则发送一条resolveInstanceMethod消息,即执行resolveInstanceMethod方法。
- 完成消息发送后,会再进行
saySomething2方法的查找,但是依然找不到!因为JHSPerson虽然实现了resolveInstanceMethod,但是里面什么也没有做!
7.
resolveInstanceMethod执行完成后,回到resolveMethod_locked流程中,调用lookUpImpOrForwardTryCache再次进行方法查找。
8.在lookUpImpOrForwardTryCache中,依然查找saySomething2,此时会从缓存中返回forward_imp,也就是进行消息转发。
- 动态方法决议流程结束,只是此案例中,虽然实现了
动态方法决议,但是里面什么也没有做,进入消息转发,最终运行结果报错!
总结:通过上面的案例,理清楚了动态方法决议的流程。但是实际开发过程中,我们肯定会抓出这次处理错误的机会。下面我们对案例进行修改,将sayHello方法指向其他方法。
4.案例深入探索
依然是上面的案例,但是我们抓住这次机会,向类中添加一个方法,方法的sel依然是saySomething2,但是其对应的方法实现imp为saySomething1的实现。
// 动态方法决议 - 给一次机会, 需要进行方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"给你一次机会...");
if (sel == @selector(saySomething2)) {
IMP imp = class_getMethodImplementation(self, @selector(saySomething1));
Method method = class_getInstanceMethod(self, @selector(saySomething1));
class_addMethod(self, sel, imp, method_getTypeEncoding(method));
}
return [super resolveInstanceMethod:sel];
}
运行上面的代码,这次有什么不同呢?我们依然把关注点放到resolveInstanceMethod方法中,跟踪1、2、3三个地方分别作了什么?
- 调用
JHSPerson类的实例方法saySomething2,分别进行快速查找和慢速查找,均找不到该方法。最终会进入源码的resolveMethod_locked->resolveInstanceMethod流程中。 - 代码运行到
1处,会查找类是否实现了resolveInstanceMethod,如果实现了,则会将该方法插入缓存,以便下次进行快速方法查找;如果没有实现,直接返回。此处流程和初探时的流程是一致的! - 代码运行到
2处,发送msg,即执行JHSPerson 类中的resolveInstanceMethod方法。由于将saySomething2方法指向了saySomething1,则此处class_addMethod会将方法插入class_rw_ext_t,也就是插入JHSPerson的方法列表中; 方法插入流程,见下图:
至此,动态方法决议方法已经执行一次,并重新设定了方法实现。
- 代码运行到
3处,再次查找saySomething2,此流程会在方法列表中查找到方法实现saySomething1,并以sel=saySomething2,imp=saySomething1实现的形式插入方法缓存。
3处调用路径:lookUpImpOrNilTryCache -> _lookUpImpTryCache -> lookUpImpOrForward。
- 进入
lookUpImpOrForward,查找sayHello方法。
最终在方法列表中找到了,并将其插入缓存中。
- 继续运行代码,回到
resolveMethod_locked,并再次调用lookUpImpOrForwardTryCache方法,进行方法查找。
- 此时,通过
cache_getImp找到了方法实现,方法实现为sayHello1,返回imp。
5.类方法动态决议
针对类方法,与实例方法类似,同样可以通过重写resolveClassMethod类方法来解决前文的崩溃问题,即在JHSPerson类中重写该方法,并将say666类方法的实现指向类方法say6661
// 动态方法决议 - 给一次机会, 需要进行方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"给你一次机会...");
if (sel == @selector(saySomething2)) {
IMP imp = class_getMethodImplementation(self, @selector(saySomething1));
Method method = class_getInstanceMethod(self, @selector(saySomething1));
class_addMethod(self, sel, imp, method_getTypeEncoding(method));
}
if (sel == @selector(saySomething3)) {
Class meteCls = objc_getMetaClass("JHSPerson");
IMP imp = class_getMethodImplementation(meteCls, @selector(saySomething4));
Method method = class_getInstanceMethod(meteCls, @selector(saySomething4));
return class_addMethod(meteCls, sel, imp, method_getTypeEncoding(method));
}
// 什么也没做
return [super resolveInstanceMethod:sel];
}
resolveClassMethod类方法的重写需要注意一点,传入的cls不再是类,而是元类,可以通过objc_getMetaClass方法获取类的元类,原因是因为类方法在元类中是实例方法。
3.动态方法决议使用优化
上面的这种方式是单独在每个类中重写,有没有更好的,一劳永逸的方法呢?其实通过方法慢速查找流程可以发现其查找路径有两条:
实例方法:类 -- 父类 -- 根类 -- nil类方法:元类 -- 根元类 -- 根类 -- nil它们的共同点是如果前面没找到,都会来到根类即NSObject中查找,所以我们是否可以将上述的两个方法统一整合在一起呢?答案是可以的,可以通过NSObject添加分类的方式来实现统一处理,而且由于类方法的查找,在其继承链,查找的也是实例方法,所以可以将实例方法和类方法的统一处理放在resolveInstanceMethod方法中,如下所示:
// NSObject分类
@implementation NSObject (JHS)
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(saySomething2)) {
IMP imp = class_getMethodImplementation(self, @selector(saySomething1));
Method method = class_getInstanceMethod(self, @selector(saySomething1));
class_addMethod(self, sel, imp, method_getTypeEncoding(method));
}
if (sel == @selector(saySomething3)) {
Class meteCls = objc_getMetaClass("JHSPerson");
IMP imp = class_getMethodImplementation(meteCls, @selector(saySomething4));
Method method = class_getInstanceMethod(meteCls, @selector(saySomething4));
return class_addMethod(meteCls, sel, imp, method_getTypeEncoding(method));
}
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方法才行。根据以上两点,完成下面的案例:
@interface JHSPerson : NSObject
- (void)saySomething1;
- (void)saySomething2;
@end
@implementation JHSPerson
- (void)saySomething1{
NSLog(@"saySomething1");
}
@end
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
instrumentObjcMessageSends(YES);
JHSPerson *person = [JHSPerson alloc];
[person saySomething2];
instrumentObjcMessageSends(NO);
NSLog(@"Hello, World!");
}
return 0;
}
志打印在哪里呢?在logMessageSend 源码实现中,已经告诉我们了。 snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ()); 运行以上代码,然后Finder中前往/tmp/msgSends/,就可以找到运行日志,见下图:
- 动态方法决议:
resolveInstanceMethod - 快速消息转发:
forwardingTargetForSelector - 慢速消息转发:
methodSignatureForSelector
6.快速消息转发
通过日志我们了解到forwardingTargetForSelector对象方法实际调用者是JHSPerson对象,所以在JHSPerson类中添加对象方法的forwardingTargetForSelector实现,依然调用JHSPerson的对象方法saySomething2。
// 动态方法决议 - 给一次机会, 需要进行方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"给你一次机会...");
// if (sel == @selector(saySomething2)) {
// IMP imp = class_getMethodImplementation(self, @selector(saySomething1));
// Method method = class_getInstanceMethod(self, @selector(saySomething1));
// class_addMethod(self, sel, imp, method_getTypeEncoding(method));
// }
// if (sel == @selector(saySomething3)) {
// Class meteCls = objc_getMetaClass("JHSPerson");
// IMP imp = class_getMethodImplementation(meteCls, @selector(saySomething4));
// Method method = class_getInstanceMethod(meteCls, @selector(saySomething4));
// return class_addMethod(meteCls, sel, imp, method_getTypeEncoding(method));
// }
return [super resolveInstanceMethod:sel];
}
// 快速消息转发
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"给你二次机会...%@", NSStringFromSelector(aSelector));
return [super forwardingTargetForSelector:aSelector];
}
运行结果发现,依然崩溃,因为依然没有找到方法实现。但是在快速消息转发中的打印结果为:给你二次机会...saySomething2。也就是说在错过第一次补救机会动态方法决议后,快速消息转发forwardingTargetForSelector会给我们第二次的补救机会。
这次机会我们可以理解为甩锅,简单理解为:我处理不了了,你让别人帮我处理吧! 现在对上面的案例进行一些修改,添加一个JHSTeacher类,声明并实现saySomething5方法。
@interface JHSTeacher : NSObject
- (void)saySomething2;
- (void)saySomething6;
@end
@implementation JHSTeacher
- (void)saySomething2{
NSLog(@"saySomething2 %s",__func__);
}
- (void)saySomething6{
NSLog(@"saySomething3 %s",__func__);
}
@end
JHSPerson类的快速消息转发中,将方法甩锅给JHSTeacher对象。也就是抓住这次机会,重新设置一个方法接受者。
// 快速消息转发
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"给你二次机会...%@", NSStringFromSelector(aSelector));
return [JHSTeacher alloc];
// return [super forwardingTargetForSelector:aSelector];
}
运行后不再崩溃,并成功调用了JHSTeacher中的saySomething2方法。说明这次甩锅起到了效果,将方法的接受者变成了JHSTeachern对象。需要说明的是,在JHSTeacher中寻找saySomething2方法时,依然会走快速查找、慢速查找、动态方法决议等流程。
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 = JHSTeacher、selector = saySomething6。
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"forwardInvocation %s", __func__);
anInvocation.target = [JHSTeacher alloc];
anInvocation.selector = @selector(saySomething6);
[anInvocation invoke];
}
运行结果见下图。不崩溃,并且从一开始的调用JHSPerson对象的saySomething2,到成功调用JHSTeacher对象的saySomething6方法。
8.总结
到目前为止,objc_msgSend发送消息的流程就分析完成了,在这里简单总结下。
快速查找流程:首先,在类的缓存cache中查找指定方法的实现。慢速查找流程:如果缓存中没有找到,则在类的方法列表中查找,如果还是没找到,则去父类链的缓存和方法列表中查找。动态方法决议:如果慢速查找还是没有找到时,第一次补救机会就是尝试一次动态方法决议,即重写resolveInstanceMethod/resolveClassMethod 方法。消息转发:如果动态方法决议还是没有找到,则进行消息转发,消息转发中有两次补救机会:快速转发、慢速转发。- 如果转发之后也没有,则程序直接报错崩溃
unrecognized selector sent to instance。