在前面两篇文章iOS-底层原理 objc_msgSend流程分析之慢速查找和objc_msgSend流程分析之缓存查找,分别分析了objc_msgSend的快速查找和慢速查找,在这两种都没找到方法实现的情况下,苹果给了两个建议
- 动态方法决议:慢速查找流程未找到后,会执行一次动态方法决议
- 消息转发:如果动态方法决议仍然没有找到实现,则进行消息转发
方法未实现报错详解
报错如图
方法未实现报错源码
根据慢速查找的源码,我们发现,其报错最后都是走到**__objc_msgForward_impcache方法**,以下是报错流程的源码
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;
看着objc_defaultForwardHandler有没有很眼熟,这就是我们在日常开发中最常见的错误:没有实现函数,运行程序,崩溃时报的错误提示。
防止方法未实现的崩溃。
三次方法查找的挽救机会
【第一次机会】动态方法决议->消息转发流程
【第二次机会】快速转发
【第三次机会】慢速转发
动态方法决议
在慢速查找流程未找到方法实现时,**首先会尝试一次动态方法决议,**其源码实现如下:
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 (!lookUpImpOrNil(inst, sel, cls)) { //如果没有找到或者为空,在元类的对象方法解析方法中查找
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
//如果方法解析中将其实现指向其他方法,则继续走方法查找流程
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
主要分为以下几步
判断类是否是元类
- 如果是类,执行实例方法的动态方法决议resolveInstanceMethod
- 如果是元类,执行类方法的动态方法决议resolveClassMethod,如果在元类中没有找到或者为空,则在元类的实例方法的动态方法决议resolveInstanceMethod中查找,主要是因为类方法在元类中是实例方法,所以还需要查找元类中实例方法的动态方法决议
- 如果动态方法决议中,将其实现指向了其他方法,则继续查找指定的imp,即继续慢速查找lookUpImpOrForward流程
实例方法
针对实例方法调用,在快速-慢速查找均没有找到实例方法的实现时,我们有一次挽救的机会,即尝试一次动态方法决议,由于是实例方法,所以会走到resolveInstanceMethod方法,其源码如下
在resolveInstanceMethod调用后,又调用了一次lookUpImpOrNil;我们知道该方法如果找到对应imp之后会插入到对象类的缓存中去,方便后续使用;另一个是方便debug进行打印。
主要分为以下几个步骤:
- 在发送resolveInstanceMethod消息前,需要查找cls类中是否有该方法的实现,即通过lookUpImpOrNil方法又会进入lookUpImpOrForward慢速查找流程查找resolveInstanceMethod方法
- 如果没有,则直接返回 如果有,则发送
- resolveInstanceMethod消息
- 再次慢速查找实例方法的实现,即通过lookUpImpOrNil方法又会进入lookUpImpOrForward慢速查找流程查找实例方法
实例方法的resolve
为self动态增加sayMaster的实现.使用runtime-api
+ (BOOL)resolveInstanceMethod:(SEL)sel{
IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
Method sayMMethod = class_getInstanceMethod(self, @selector(sayMaster));
const char *type = method_getTypeEncoding(sayMMethod);
return class_addMethod(self, sel, imp, type);
}
类方法
针对类方法,与实例方法类似,同样可以通过重写resolveClassMethod类方法来解决前文的崩溃问题,
+ (BOOL)resolveClassMethod:(SEL)sel{
IMP imp = class_getMethodImplementation(objc_getMetaClass("ZMPerson"), @selector(lgClassMethod));
Method sayMMethod = class_getInstanceMethod(objc_getMetaClass("ZMPerson"), @selector(lgClassMethod));
const char *type = method_getTypeEncoding(sayMMethod);
return class_addMethod(objc_getMetaClass("Person"), sel, imp, type);
}
resolveClassMethod类方法的重写需要注意一点,传入的cls不再是类,而是元类,可以通过objc_getMetaClass方法获取类的元类,原因是因为类方法在元类中是实例方法
实例方法与类方法的区别
唯一的区别就是类方法是需要添加到元类中的,所以需要先找到元类objc_getMetaClass.
根据观察resolveInstanceMetho会走2次
- 第一次是在查询方法时lookupimp中调用的
- 实例方法在动态方法决议时执行的是resolveInstanceMethod方法进行补救。
- 类方法在动态方法决议时执行的是resolveClassMethod和resolveInstanceMethod方法进行补救。
resolveClassMethod源码实现
实现逻辑于resolveInstanceMethod基本相同
在NSObject中实现resolveInstanceMethod,也可以解决类方法找不到问题
- 在源码中,如果是元类的话,会有下面这行代码,如果在没有找到类方法,那么就会在元类的对象方法解析方法中查找。
if (!lookUpImpOrNil(inst, sel, cls)) { //如果没有找到或者为空,在元类的对象方法解析方法中查找
resolveInstanceMethod(inst, sel, cls);
}
- · 在isa走位图中,继承关系是:元类 -> 根元类 -> NSObject ->
nil。元类最终继承于NSObject,那么我们在NSObject中实现resolveInstanceMethod,那么类方法也会最终执行到NSObject中。
NSObject分类
@implementation NSObject (LG)
// 调用方法的时候 - 分类
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"%@ 来了",NSStringFromSelector(sel));
if (sel == @selector(say666)) {
NSLog(@"%@ 来了",NSStringFromSelector(sel));
IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
Method sayMMethod = class_getInstanceMethod(self, @selector(sayMaster));
const char *type = method_getTypeEncoding(sayMMethod);
return class_addMethod(self, sel, imp, type);
}
return NO;
}
@end
【第二次机会】快速转发
针对前文的崩溃问题,如果动态方法决议也没有找到实现,则需要在ZMPerson中重写forwardingTargetForSelector方法,将ZMPerson的实例方法的接收者指定为ZMSon的对象(ZMSon类中有say666的具体实现),如下所示
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
// runtime + aSelector + addMethod + imp
//将消息的接收者指定为LGStudent,在LGStudent中查找say666的实现
return [ZMSon alloc];
}
也可以直接不指定消息接收者,直接调用父类的该方法,如果还是没有找到,则直接报错
【第三次机会】慢速转发
针对第二次机会即快速转发中还是没有找到,则进入最后的一次挽救机会,即在ZMPerson中重写methodSignatureForSelector,如下所示
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s - %@",__func__,anInvocation);
}
所以,由上述可知,无论在forwardInvocation方法中是否处理invocation事务,程序都不会崩溃。
总结
到目前为止,objc_msgSend发送消息的流程就分析完成了,在这里简单总结下
【快速查找流程】首先,在类的缓存cache中查找指定方法的实现
【慢速查找流程】如果缓存中没有找到,则在类的方法列表中查找,如果还是没找到,则去父类链的缓存和方法列表中查找
【动态方法决议】如果慢速查找还是没有找到时,第一次补救机会就是尝试一次动态方法决议,即重写resolveInstanceMethod/resolveClassMethod 方法
【消息转发】如果动态方法决议还是没有找到,则进行消息转发,消息转发中有两次补救机会:快速转发+慢速转发
如果转发之后也没有,则程序直接报错崩溃unrecognized selector sent to instance