前言
在开发过程中,有这么一个现象,如果在类里面调用了不存在的方法,或者是调用了未实现的方法,就会报错,如:...unrecognized selector sent to instance...
。作为一名开发者,总是对不知道的东西充满神秘感和保持好奇心。就是,为什么调用不存在的方法会报这样的错误了?接下来,就以这个点为突破口,进行探究,会让童鞋们发现不一样的精彩。
资源准备
- objc源码:多个版本的objc源码
- 冰?
进入主题
当类调用到未找到的方法,就报未找到该方法的错误,既然是方法查找,再根据上篇文章方法慢速查找流程,那么就是直接定位到objc
底层lookUpImpOrForward()
方法上了。当本类
以及本类的所有父类
里面,都找不到对应方法的时候,就会返回一个forward_imp
,那么故事就从这里开始了。
方法找不到报错的底层原理
方法找不到时,返回的imp
是forward_imp
因为在上一篇文章里面,详细的分析了这块源码,所以这里就不再把源码全部贴出来。只把在这篇文章分析所需要的源码给贴出来。根据源码中的注释,方法慢速查找
流程的四
个步骤:①、②、③、④
查看源码
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
//-------------------代码省略..............
for (unsigned attempts = unreasonableClassCount();;) {
//---- ①、先在缓存中查找一次,因为是为了防止所查找的方法,在类初始化的时候,就已经加入缓存中了,所以先找一遍以防万一
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
Method meth = getMethodNoSuper_nolock(curClass, sel);//---- ②、通过二分法查找,获取当前方法
if (meth) {//找到了方法
imp = meth->imp(false);
goto done;//写入缓存
}
//---- ④、如果所有的父类都找完了,还是没有的话,会返回一个外传的imp
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
imp = forward_imp;
break;
}
}
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
//----③、如果当前子类找不到,就直接到父类里面找,父类里面查找也分为快速和慢速两个流程.
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (slowpath(imp == forward_imp)) {
break;
}
if (fastpath(imp)) {
goto done;//缓存填充
}
}
//-------------------------- 代码省略...............
return imp;
}
复制代码
由于在本类
以及本类的所有父类
里面,都找不到对应方法,所以imp
返回了一个forward_imp
,而根据源码,forward_imp
的定义就是_objc_msgForward_impcache
。
由forward_imp
的定义进行跟踪
那么全文索引_objc_msgForward_impcache
,查看他的实现,继续找线索 (经验提示:一般带下划线的,大概率找汇编)。真机环境—arm64
:
此时,我们通过源码看到,在_objc_msgForward_impcache
里面只执行跳转__objc_msgForward
的方法,那么再看这个跳转的方法。
在__objc_msgForward
的方法中,汇编指令adrp
表示,将以页为单位__objc_forward_handler
的地址取到 x17
寄存器里面,而ldr
指令是将__objc_forward_handler
地址所指向的值赋给x17
寄存器,最后再执行TailCallFunctionPointer x17
。
而我们再TailCallFunctionPointer
的宏定义:
.macro TailCallFunctionPointer
// $0 = function pointer value
braaz $0
.endmacro
复制代码
发现,只是跳转到$0
,带到源码里面执行,就是要跳转到x17
寄存器,而x17
的地址就是__objc_forward_handler
,也就是接下来的线索。
forward_imp
最终指向objc_defaultForwardHandler()
当全文索引__objc_forward_handler
时,没有找到他的实现,那么证明此方法不再是汇编方法,而是C++
方法,去掉头部下划线,直接搜索objc_forward_handler
。最终在objc-runtime.mm文件里面找到
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
作为值,传递给*_objc_forward_handler
,当看到值objc_defaultForwardHandler
的实现时,是不是有种熟悉的感觉了,我们调用不存在的方法的错,是不是就是这样的格式。class_isMetaClass(object_getClass(self))
判断是类方法还是实例方法,打印了类名object_getClassName(self)
,还打印了方法名sel_getName(sel)
,以及类地址打印self
。
报错底层原理小结
-
当方法在
本类
以及本类的所有父类
里面,都找不到的时候,imp
返回的是forward_imp
,而forward_imp
的定义是_objc_msgForward_impcache
; -
_objc_msgForward_impcache
跳转到__objc_msgForward
,而__objc_msgForward
返回到C++方法objc_forward_handler()
; -
objc_forward_handler()
由objc_defaultForwardHandler
传值,在objc_defaultForwardHandler
里面,就是报错打印的全部信息。
到了这里,我们就很清晰的知道这个报错的底层原理,我们知道当前的方法发生了错误,当错误找到之后,就不能进行其他的处理了吗?
既然提出来了,显然是有处理的方式方法的,而处理的方式方法,就涉及到一个重要的点—-消息的处理流程
。
方法动态决议—对象方法动态决议
先做些基础建设,在objc源码
中,创建一个LGPerson
类,再创建一个LGTeacher
类继承于LGPerson
,如下面源码展示:
//创建一个LGPerson类
#import <Foundation/Foundation.h>
@interface LGPerson : NSObject
- (void)say666;
@end
#import "LGPerson.h"
@implementation LGPerson
@end
//创建一个LGTeacher类
#import "LGPerson.h"
@interface LGTeacher : LGPerson
@end
#import "LGPerson.h"
@implementation LGPerson
@end
复制代码
然后在main.m
中初始化LGTeacher
类,并调用父类LGPerson
的say666
方法。
当执行到断点处时,再到底层方法查找函数lookUpImpOrForward()
中再打上断点。再跳过main.m
里面的那个断点,就会执行到lookUpImpOrForward()
中的断点处,然后再进行lldb
调试,确认当前的类是不是LGTeacher
:
确定了是LGTeacher
类后,再在下图处进行断点,查看下imp
的内容:
通过lldb
调试,发现imp
中是空的。断点这块代码是一个单利
(详情去看:补充1
),只能执行一次。
imp
为nil
时,给了一次修正机会
执行到这个单利里面后,进入resolveMethod_locked()
方法里面:
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);
}
}
// ---- ①
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
复制代码
因为此时,imp
是为nil
的,那么按照常规流程走下去,程序就直接奔溃了。然而,苹果系统开发设计者,认为这样直接奔溃不太友善,对用户的体验也不怎么好。所以,就给iOS开发者
一次拯救自己代码的机会(在①注释
之间)。也就意味着,在接下来能够对imp
进行处理,能给到一个不为空的imp
返回出去,这样就能保证程序不再奔溃。
当有了一个不为空的imp
时,通过lookUpImpOrForwardTryCache()
方法返回出去。继续跟踪去查看是如何返回的:进入lookUpImpOrForwardTryCache()
–>_lookUpImpTryCache()
,_lookUpImpTryCache()
源码实现:
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
//---- 当imp为NULL时,再次查找一次
if (slowpath(imp == NULL)) {
return lookUpImpOrForward(inst, sel, cls, behavior);
}
done:
if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
return nil;
}
return imp;
}
复制代码
- 也就是说,当
imp
为nil
时,只要我们把imp
进行处理了,那么系统会再次帮我们进行查找一次。虽然说是会消耗一些性能。
查找不到实例方法,动态处理imp
根据上文中,处理imp
的代码块(在①注释
之间),当前我们是在LGTeacher
类中,并不是元类,所以进入if
判断里面,进入resolveInstanceMethod()
方法。查看其实现:
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
//----- resolve_sel消息里,需要实现的方法
SEL resolve_sel = @selector(resolveInstanceMethod:);
if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
// Resolver not implemented.
return;
}
//---- 系统自动发送resolve_sel消息给开发者,指明了类和方法名,只要实现了resolve_sel消息里的方法,就不会再报错
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
//---- 判断resolveInstanceMethod是否执行成功
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);
//。。。。。。。。
}
复制代码
-
也就是说,当系统检测到查找的
imp
为nil
时,系统自动发送resolve_sel
消息给开发者,只要实现了resolve_sel
消息里的方法,就不会再报错,而实现的方法就是resolveInstanceMethod:
。 -
当判断成功执行
resolveInstanceMethod:
方法之后,就执行lookUpImpOrNilTryCache
方法,继续去再次查找一遍。
当然,就算不实现resolveInstanceMethod:
方法,也不会执行if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true)))
这个判断,因为,既要系统去查找resolveInstanceMethod
方法,如果不是实现,系统自己又要去报错,那么会造成系统的更加不稳定。所以系统对resolveInstanceMethod
方法默认的设置:
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;
}
复制代码
知道需要实现resolveInstanceMethod
方法后,那么此时,就可以在LGTeacher
类里面,创建一个sayNB
的方法,调用resolveInstanceMethod:
方法。动态添加一个imp
#import "LGPerson.h"
@interface LGTeacher : LGPerson
- (void)sayNB;
@end
#import "LGTeacher.h"
#import <objc/message.h>
@implementation LGTeacher
- (void)sayNB{
NSLog(@"%@ - %s",self , __func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
// 处理 sel -> imp
if (sel == @selector(say666)) {
IMP sayNBImp = class_getMethodImplementation(self, @selector(sayNB));
Method method = class_getInstanceMethod(self, @selector(sayNB));
const char *type = method_getTypeEncoding(method);
return class_addMethod(self, sel, sayNBImp, type);
}
NSLog(@"resolveInstanceMethod :%@-%@",self,NSStringFromSelector(sel));
return [super resolveInstanceMethod:sel];
}
@end
复制代码
调用resolveInstanceMethod
方法,既然没有say666
的方法,那么就有创建实现好的sayNB
的方法替换上去。运行后,没有再次报错。那么就处理成功了。
- 注:在调用
resolveInstanceMethod
方法时,NSLog(@"resolveInstanceMethod :%@-%@",self,NSStringFromSelector(sel));
会执行两次
,为什么会这样,可以直接去看:补充2
的详情。
对象方法动态决议小结
-
当查找不到某对象方法时,
lookUpImpOrForward()
返回imp
为nil
,然后就进入到一个单利
的判断里面,执行resolveMethod_locked()
; -
在
resolveMethod_locked()
里面,可以知道系统给了处理imp
的机会,只要对imp
进行动态处理,能给到一个不为空的imp
返回出去,那么系统会再次帮我们进行查找一次,这样就能保证程序不再奔溃。 -
动态处理
imp
的流程是,在类里面调用resolveInstanceMethod
方法,把空的imp
传一个已经存在的方法的imp
,使之不在为空,相当于用这个已存在的方法进行替换。这样就完成了imp
的动态处理。 -
当判断完
resolveInstanceMethod
正确执行后,bool resolved
,当执行成功之后,返回到lookUpImpOrNilTryCache()
里面,再次查找,有了imp
,就能找到并返回对应方法。
方法动态决议—类方法的动态决议
在resolveMethod_locked()
方法里面,当不是在元类的时候,处理imp
是进入if的判断
执行的,那么当在元类的时候了,处理imp
应该是进入else
判断了。接下来,在LGPerson
类里面再创建类方法:
#import <Foundation/Foundation.h>
@interface LGPerson : NSObject
- (void)say666;
+ (void)sayHappy;
@end
//在main.m里面调用
int main(int argc, const char * argv[]) {
@autoreleasepool {
[LGTeacher sayHappy];
}
return 0;
}
复制代码
最后还是来到else
的判断里面;
查找不到类方法,动态处理imp
接着就进入到resolveClassMethod()
方法里面,查看其实现,可以发现,和我们刚刚分析的实例方法动态决议很相似:
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());
//---- 判断在执行lookUpImpOrNilTryCache,有没有实现resolveClassMethod方法
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;
//---- 是否已经执行resolveClassMethod,但是resolveClassMethod是执行在类里面的,因为在类里面执行类方法,就等同于在元类里面执行实例方法
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);
//。。。。。。。。。。。。
}
复制代码
-
那么
resolveClassMethod()
方法,也就是多了对元类进行局部操作,防止其未实现,剩下的,也是当系统检测到查找的imp
为nil
时,系统自动发送resolve_sel
消息给开发者,然后就需要实现resolveInstanceMethod:
方法。 -
当判断成功执行
resolveInstanceMethod:
方法之后,就执行lookUpImpOrNilTryCache
方法,继续去再次查找一遍。 -
这里
resolveClassMethod
是执行在类
里面的,因为在类
里面执行类方法
,就等同于在元类
里面执行实例方法
。
那么接下来,就是在LGTeacher
类里面实现resolveClassMethod
方法了。
#import "LGTeacher.h"
#import <objc/message.h>
@implementation LGTeacher
+ (void)sayScott{
NSLog(@"%@ - %s",self , __func__);
}
//元类的以对象方法的方法
+ (BOOL)resolveClassMethod:(SEL)sel{
NSLog(@"resolveClassMethod :%@-%@",self,NSStringFromSelector(sel));
if (sel == @selector(sayHappy)) {
IMP sayNBImp = class_getMethodImplementation(objc_getMetaClass("LGTeacher"), @selector(sayScott));
Method method = class_getInstanceMethod(objc_getMetaClass("LGTeacher"), @selector(sayScott));
const char *type = method_getTypeEncoding(method);
return class_addMethod(objc_getMetaClass("LGTeacher"), sel, sayNBImp, type);
}
return [super resolveClassMethod:sel];
}
@end
复制代码
和类方法的处理方式最大的不同是,不是从self
里面获取,而是从objc_getMetaClass("LGTeacher")
元类里面获取。运行结果:
执行了sayScott
方法,也就是imp
处理成功。
再次执行resolveInstanceMethod
的原因
在else判断
里面,执行了resolveClassMethod
方法后,接下来还得执行一个关于inst
是否存在的判断if (!lookUpImpOrNilTryCache(inst, sel, cls))
,最后再执行了一次实例方法
动态处理imp
的方法resolveInstanceMethod
,这是为什么了?
- 因为,这是为了判断当前的类里面,是否存在
resolveInstanceMethod
方法。
根据isa的走位图
(详情见:补充2
),可以知道,类里面的类方法,在元类里面是以实例方法存储的。那么在类里面是以类方法通过resolveClassMethod
方法动态处理imp
,同时还可以在元类里面,以实例方法通过resolveInstanceMethod
方法动态处理imp
。这就是为什么要再次执行resolveInstanceMethod
方法的原因。
类方法动态决议小结
-
当查找不到某类方法时,
lookUpImpOrForward()
返回imp
为nil
,然后就进入到一个单利
的判断里面,执行resolveMethod_locked()
; -
在
resolveMethod_locked()
里面,执行else判断部分
,进行动态处理imp
,处理的操作,首先是对类里面的类方法进行查询,执行resolveClassMethod
方法,当完成imp的动态处理之后,还要进行元类
的查询处理。 -
因为类里面执行类方法,就等同于在元类里面执行实例方法,所以判断
inst
是否存在,如果存在,就要再次调用resolveInstanceMethod
方法,在元类里面再处理一次。 -
当判断完
resolveInstanceMethod
正确执行后,bool resolved
,当执行成功之后,返回到lookUpImpOrNilTryCache()
里面,再次查找,有了imp
,就能找到并返回对应方法。
用NSObject分类
囊括实例方法
和类方法
的动态处理imp
既然在处理实例方法和类方法,都是要用到这个,而NSObject类
是跟父类,那么直接就可以用NSObject的分类
来实现:
#import "NSObject+LG.h"
#import <objc/message.h>
@implementation NSObject (LG)
- (void)sayNB{
NSLog(@"%@ - %s",self , __func__);
}
+ (void)sayScott{
NSLog(@"%@ - %s",self , __func__);
}
#pragma clang diagnostic push
// 让编译器忽略错误
#pragma clang diagnostic ignored "-Wundeclared-selector"
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"resolveInstanceMethod :%@-%@",self,NSStringFromSelector(sel));
if (sel == @selector(say666)) {
IMP sayNBImp = class_getMethodImplementation(self, @selector(sayNB));
Method method = class_getInstanceMethod(self, @selector(sayNB));
const char *type = method_getTypeEncoding(method);
return class_addMethod(self, sel, sayNBImp, type);
}else if (sel == @selector(sayHappy)) {
IMP sayNBImp = class_getMethodImplementation(objc_getMetaClass("LGTeacher"), @selector(sayScott));
Method method = class_getInstanceMethod(objc_getMetaClass("LGTeacher"), @selector(sayScott));
const char *type = method_getTypeEncoding(method);
return class_addMethod(objc_getMetaClass("LGTeacher"), sel, sayNBImp, type);
}
return NO;
}
@end
复制代码
这样就能一步到位了。
补充
补充1
、位运算单利
的计算过程
为什么说一个单利
了?
behavior
是由lookUpImpOrForward()
传入的,根据其底层汇编,知道,behavior = 3
;
再来看LOOKUP_RESOLVER
,是个枚举值,LOOKUP_RESOLVER = 2
:
带入到if
判断里面去运算,behavior = behavior & LOOKUP_RESOLVER = 3 & 2 = 2
,接着再进入判断里面运算,behavior ^= LOOKUP_RESOLVER
转化下就behavior = behavior ^ LOOKUP_RESOLVER = 2 ^ 2 = 0
。当behavior = 0
之后,下次再执行这个判断时,behavior & 任何数
,都是为0
的,所以,就再也进入不了判断里面,也就是只执行了一次。behavior
标记当前的行为是LOOKUP_RESOLVER
。
以behavior
的传值为线索,进入resolveMethod_locked()
,再进入lookUpImpOrForwardTryCache()
,会看到
IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
{
//---- 此处可以看到,behavior | LOOKUP_NIL是再次动态默认赋值,意味着当前是一个新的操作,behavior的行为被标记为LOOKUP_NIL
return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
}
复制代码
- 这个位运算的判断,就是一个
标记
的过程。
补充2
、执行两次
的原因
在调用say666
方法时,找不到该实例方法,动态处理imp
时,执行resolveInstanceMethod方法时,打印了两次。
在objc底层
中,不存在类方法,只有实例方法,类方法是在OC层
才有的,所以,在OC层
中,类方法是以实例方法的形式存储在元类
当中。根据苹果官网给的isa走位图
可以看出:
类调用实例方法,查找的方式可以是从本类,再到父类这样进行一次查找,还能在元类到父元类这样一次查找,最后,都是到达NSObject,所以执行了两次。