一、方法找不到的报错底层原理
如果一个方法只有声明没有实现,则会报出经典错误
:
unrecognized selector sent to instance
那么这个错误是怎么报出来的呢?
在lookUpImpOrForward
函数中,当父类为nil
的时候,imp
被赋值为forward_imp
,而forward_imp
为_objc_msgForward_impcache
!
那么我们进入_objc_msgForward_impcache
:
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
再进入__objc_msgForward
:
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
TailCallFunctionPointer
之前探索过,就是返回$0
,即x17
,而x17
是在__objc_forward_handler
:
// 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;
这样就找到了经典方法找不到的错误:unrecognized selector sent to instance
!
从这里也可以看出,底层根本就没有对象方法
和类方法
的区分!只是判断self的类
是不是元类
!在元类则为类方法
,不在元类则为对象方法
!
那是不是方法找不到就直接报错了呢?
并不是,方法找不到就会进入方法处理流程
!
二、对象方法的动态决议
1、特殊标志behavior
在lookUpImpOrForward
函数中没有找到方法的后会进入一个判断
:
// No implementation found. Try method resolver once.
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
behavior
是之前快速查找
时传入的:
.macro MethodTableLookup
SAVE_REGS MSGSEND
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
mov x2, x16
mov x3, #3
bl _lookUpImpOrForward
// IMP in x0
mov x17, x0
RESTORE_REGS MSGSEND
.endmacro
即behavior
为3
!
搜索LOOKUP_RESOLVER
:
/* method lookup */
enum {
LOOKUP_INITIALIZE = 1,
LOOKUP_RESOLVER = 2,
LOOKUP_NIL = 4,
LOOKUP_NOCACHE = 8,
};
即LOOKUP_RESOLVER
为2
,所以此时的判断为:
behavior & LOOKUP_RESOLVER = 3 & 2 = 0x11 & 0x10 = 0x10 = 2
满足
判断的条件!
接着:
behavior ^= LOOKUP_RESOLVER
=> behavior = behavior ^ LOOKUP_RESOLVER = 3 ^ 2 = 0x11 ^ 0x10 = 0x01 = 1
那么在不改变behavior的值的情况下,下次再进入到这个判断:
behavior & LOOKUP_RESOLVER = 1 & 2 = 0x01 & 0x10 = 0x00 = 0
即无法进入判断!
说明该判断一般情况只能进入一次!
2、resolveMethod_locked
接着进入到resolveMethod_locked
函数:
/***********************************************************************
* resolveMethod_locked
* Call +resolveClassMethod or +resolveInstanceMethod.
*
* Called with the runtimeLock held to avoid pressure in the caller
* Tail calls into lookUpImpOrForward, also to avoid pressure in the callerb
**********************************************************************/
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);
}
先看return
即lookUpImpOrForwardTryCache
函数,根据名字可知是重新去缓存里面找方法:
IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{
return _lookUpImpTryCache(inst, sel, cls, behavior);
}
再进入到_lookUpImpTryCache
函数:
/***********************************************************************
* lookUpImpOrForward / lookUpImpOrForwardTryCache / lookUpImpOrNilTryCache
* The standard IMP lookup.
*
* The TryCache variant attempts a fast-path lookup in the IMP Cache.
* Most callers should use lookUpImpOrForwardTryCache with LOOKUP_INITIALIZE
*
* Without LOOKUP_INITIALIZE: tries to avoid +initialize (but sometimes fails)
* With LOOKUP_NIL: returns nil on negative cache hits
*
* inst is an instance of cls or a subclass thereof, or nil if none is known.
* If cls is an un-initialized metaclass then a non-nil inst is faster.
* May return _objc_msgForward_impcache. IMPs destined for external use
* must be converted to _objc_msgForward or _objc_msgForward_stret.
* If you don't want forwarding at all, use LOOKUP_NIL.
**********************************************************************/
ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertUnlocked();
if (slowpath(!cls->isInitialized())) {
// see comment in lookUpImpOrForward
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:
if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
return nil;
}
return imp;
}
这个方法先看到是否初始化,没有则慢速查找方法,有则先快速查找方法再慢速查找方法!
为什么之前明明已经查找过方法了,而且没有找到,这里还会继续查找呢?
因为在resolveMethod_locked
函数里面做了给了最后的挽救机会:
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);
}
}
如果是类不是元类则调用resolveInstanceMethod
函数:
/***********************************************************************
* resolveInstanceMethod
* Call +resolveInstanceMethod, looking for a method to be added to class cls.
* cls may be a metaclass or a non-meta class.
* Does not check if the method already exists.
**********************************************************************/
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
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));
}
}
}
这里是给类发送消息,所以是去元类中找方法,即类方法!所以系统调用了resolveInstanceMethod
方法!
3、动态添加方法
因为是给class
发信息,所以是类方法
!
我们实现一下:
发现的确进入到了这个方法,而且进入了2次!
既然在方法报错前能够进入到这个方法,那么意味则可以在这个方法里面进行处理!比如动态添加方法!
先导入objc/runtime.h
,然后添加方法:
这就是对象方法的动态处理,这样就成功的避免了方法报错!
再回到resolveInstanceMethod
方法,里面有个if
判断:
SEL resolve_sel = @selector(resolveInstanceMethod:);
if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
// Resolver not implemented.
return;
}
那么这里是否会进入到return
呢?
并不会!
先看看lookUpImpOrNilTryCache
方法:
IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
{
return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
}
这里调用的_lookUpImpTryCache
方法,其实就是查找方法!
而resolveInstanceMethod
方法在NSObject
中就已经实现了:
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;
}
所以这个判断是不会进入的!
接着就是调用resolveInstanceMethod
方法,可以在里面进行一些处理,然后继续进行查找lookUpImpOrNilTryCache
!
因为已经添加了方法,所以再次进行查找的时候就可以找到方法并执行!
三、类方法的动态方法决议
回到resolveMethod_locked方法:
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);
}
}
如果是类方法的话,就会进入到else
!
先执行resolveClassMethod
方法:
/***********************************************************************
* resolveClassMethod
* Call +resolveClassMethod, looking for a method to be added to class cls.
* cls should be a metaclass.
* Does not check if the method already exists.
**********************************************************************/
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());
if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
// Resolver not implemented.
return;
}
Class nonmeta;
{
mutex_locker_t lock(runtimeLock);
nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
// +initialize path should have realized nonmeta already
if (!nonmeta->isRealized()) {
_objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
nonmeta->nameForLogging(), nonmeta);
}
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveClassMethod adds to self->ISA() a.k.a. cls
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
方法是否存在!
因为类方法是存在元类
里面,所以在调用resolveClassMethod
方法前,先调用getMaybeUnrealizedNonMetaClass
方法通过元类获取当前类!
然后给类发消息,和对象方法的动态决议类似,也是类方法!
实现一下:
这就是类方法的动态处理,这样就成功的避免了方法报错!
执行完resolveClassMethod
方法后,还会通过判断如果依旧没有找到,则进入到resolveInstanceMethod
方法。在resolveInstanceMethod
方法中给元类发送消息,即在元类的元类中查找方法,所以是在根元类中查找方法!
既然对象方法和类方法都会进入到resolveInstanceMethod
方法,那么是不是有一劳永逸的方式呢?
根据isa
的走位图就可以得知,查找方法最终会到NSObject
类中去查找!
所以在NSObject
的分类中实现resolveInstanceMethod
方法即可:
@implementation NSObject (KR)
- (void)sayOK{
NSLog(@"%@ -- %s", self, __func__);
}
+ (void)sayYes{
NSLog(@"%@ -- %s", self, __func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
//动态添加方法
if (sel == @selector(sayNO)) {
IMP sayYesIMP = class_getMethodImplementation(self, @selector(sayOK));
Method method = class_getInstanceMethod(self, @selector(sayOK));
const char* type = method_getTypeEncoding(method);
return class_addMethod(self, sel, sayYesIMP, type);
}else if (sel == @selector(sayHello)) {
IMP sayYesIMP = class_getMethodImplementation(objc_getMetaClass("HPerson"), @selector(sayYes));
Method method = class_getInstanceMethod(objc_getMetaClass("HPerson"), @selector(sayYes));
const char* type = method_getTypeEncoding(method);
return class_addMethod(objc_getMetaClass("HPerson"), sel, sayYesIMP, type);
}
NSLog(@"resolveInstanceMethod: %@ -- %@", self, NSStringFromSelector(sel));
return NO;
}
@end
运行一下:
成功避免了方法报错!