前言
在前面的文章中,我们讲述了方法的快速查找和慢速查找过程,如果方法找不到会做什么呢,有没有挽救的机会呢?本文将对这些问题进行探究
案例之经典报错
- 定义一个
WSPerson
类继承NSObject
,然后定义一个WSTeacher
类继承WSPerson
:
@interface WSPerson : NSObject
- (void)sayNB; // 有实现
+ (void)sayCool; // 无实现
- (void)sayLike; // 未实现
@end
@interface WSTeacher : WSPerson
- (void)sayLearn; // 有实现
+ (void)sayWrite; // 有实现
- (void)sayLike; // 未实现
@end
// 调用方法
WSTeacher *teacher = [[WSTeacher alloc] init];
[teacher sayLearn]; // WSTeacher 有实现的方法
[teacher sayNB]; // WSPerson 有实现的方法
[teacher sayLike]; // 都未实现的方法
复制代码
结果可想而知:
- 这个是我们常见的错误
unrecognized selector sent to instance xxx
,那么这个错误是怎么产生的呢,我再次去查看lookUpImpOrForward
方法分析,我们在上节课中分析了,当父类为nil
时,会进行一个赋值imp = forward_imp
,再来看看
它是一个_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
...
.macro TailCallFunctionPointer
// $0 = function pointer value
braaz $0
.endmacro
复制代码
- 这里主要操作的是
x17
,由于TailCallFunctionPointer
只是一个跳转,而上面__objc_forward_handler
的值给x17
,我们把重点放在__objc_forward_handler
,通过查找发现它在c++
代码中
__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;
复制代码
- 这里还判断了
+
还是-
方法。
没有找到
imp
就直接崩溃了,一点余地都不给,不给处理方法吗?显示是有余地的,下面我们继续去分析lookUpImpOrForward
动态方法决议
在lookUpImpOrForward
中循环查找时,如果没有找到,会走到resolveMethod_locked
方法,也就是动态方法决议
:
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
// 动态决议
return resolveMethod_locked(inst, sel, cls, behavior);
}
复制代码
behavior
是3
,LOOKUP_RESOLVER
是2
:
enum {
LOOKUP_INITIALIZE = 1,
LOOKUP_RESOLVER = 2,
LOOKUP_NIL = 4,
LOOKUP_NOCACHE = 8,
};
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
复制代码
- 得知
behavior = LOOKUP_INITIALIZE | LOOKUP_INITIALIZE = 3
- 所以可以得到
behavior & LOOKUP_RESOLVER = 3 & 2 = 2
,behavior ^= LOOKUP_RESOLVER = behavior = 3 ^ 2 = 1
,这是behavior
的值变成了1
,如果再走到判断slowpath(behavior & LOOKUP_RESOLVER)
,这时behavior & LOOKUP_RESOLVER = 1 & 2 = 0
不会进来,也就是如果没有新的behavior
值,这个方法就只进来一次,相当于单例
。 - 然后再来看看
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);
}
复制代码
- 这里先
判断是不是元类
,如果不是是就走resolveInstanceMethod
方法,如果是元类走resolveClassMethod
。 - 判断走完后,系统会
再次进行一次慢速查找
,这整个方法也就是系统给的一次处理错误的机会
- 如果都没有找到,会走
lookUpImpOrForwardTryCache
resolveInstanceMethod
先来看看resolveInstanceMethod
代码
代码分析
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:); // 系统发送的方法
// 系统对resolveInstanceMethod方法进行了默认实现,所以这个if判断不会走进来
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); //发送到当前class,
// 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了
if (resolved && PrintResolving) {
// 一些说明
}
}
复制代码
-
- 系统会将创建的
@selector(resolveInstanceMethod:)
方法,发送给当前class
,这里一的resolve_sel
一定会有值,因为系统给resolveInstanceMethod
一个默认值NO
,所以if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true)))
这里面不会走进来
- 系统会将创建的
-
- 当前给系统发送完消息后,会走到
lookUpImpOrNilTryCache
方法
IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior) { return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL); } 复制代码
这里通过
behavior | LOOKUP_NIL
给behavior
一个新值,再次调用lookUpImpOrForward
时,如果循环没找到imp
会走到那个单例子 - 当前给系统发送完消息后,会走到
代码验证
- 通过分析我们得知,系统发送
@selector(resolveInstanceMethod:)
方法到class
后,然后会往类里再查找一遍,那么我们在WSTeacher
中重写下
这个方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"resolveInstanceMethod: %@ - %@", self, NSStringFromSelector(sel));
return [super resolveInstanceMethod:sel];
}
复制代码
- 运行结果如下
运行的结果中
resolveInstanceMethod
打印是在报错之前,也就是说我们可以在里面做一些处理,例如动态添加方法。
防止报错
- 在
resolveInstanceMethod
,当调用方法为sayLike
时,调用其他方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(sayLike)) {
IMP learnImp = class_getMethodImplementation(self, @selector(sayLearn));
Method learnMethod = class_getInstanceMethod(self, @selector(sayLearn));
const char *type = method_getTypeEncoding(learnMethod);
return class_addMethod(self, sel, learnImp, type);
}
NSLog(@"resolveInstanceMethod: %@ - %@", self, NSStringFromSelector(sel));
return [super resolveInstanceMethod:sel];
}
复制代码
再来看下打印结果:
- 如果找不到
sayLike
方法,就会走sayLearn
方法 - 我们在这个发送成功后,系统的
bool resolved = msg(cls, resolve_sel, sel)
这里的值就为true
,然后再去class
找方法就能找到了。完美解决了因没有实现而报错的问题。
resolveClassMethod
看完了对象方法的动态决议
,再来看看类方法的动态决议
:
代码分析
if (! cls->isMetaClass()) {
}
else {
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());
// resolveClassMethod 有默认实现,不会在下面判断中直接return
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) {
// 打印说明
}
}
复制代码
-
- 这里基本和
resolveInstanceMethod
类似,只不过,这里是向元类发消息
- 这里基本和
-
- 处理完
resolveClassMethod
后,如果元类的Imp
存在会查找一次resolveInstanceMethod
方法。
- 处理完
代码验证
- 再来在代码中测试下,在
WSTeacher
中重写resolveClassMethod
,然后调用[WSTeacher sayCool]
:
+ (BOOL)resolveClassMethod:(SEL)sel {
NSLog(@"resolveClassMethod: %@ - %@", self, NSStringFromSelector(sel));
return [super resolveClassMethod:sel];
}
复制代码
运行结果如下:
- 说明类方法报错会走这里
- 我们再处理下,当调用
sayCool时
,调用sayReadBook
:
防止报错
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(sayCool)) {
IMP reakImp = class_getMethodImplementation(objc_getMetaClass("WSTeacher"), @selector(sayReadBook));
Method readMethod = class_getInstanceMethod(objc_getMetaClass("WSTeacher"), @selector(sayReadBook));
const char *type = method_getTypeEncoding(readMethod);
return class_addMethod(objc_getMetaClass("WSTeacher"), sel, reakImp, type);
}
NSLog(@"resolveClassMethod: %@ - %@", self, NSStringFromSelector(sel));
return [super resolveClassMethod:sel];
}
复制代码
打印结果如下:
问题:
1. 这个里面为什么不用self
?
2. 对象方法要查找resolveInstanceMethod
,为什么类的动态决议后也要查一遍?
- 解答:
-
- 因为方法要加到元类,是给元类发消息,所以要写入
WSTeacher
的元类。
- 因为方法要加到元类,是给元类发消息,所以要写入
-
- 因为类方法以对象方法的形式存在元类中,而这个对象方法也可能是在元类的父类中继承过来的,所以类的动态决议要查两遍。
-
整合类和对象的动态决议
- 通过分析我们知道
对象方法
都会走resolveInstanceMethod
方法,类方法
都会走resolveClassMethod
方法,而类和元类
最终都继承NSObject
,子类无论是对象方法
还是类方法
都不用管,在NSObject
都是对象方法。 - 所以我们可以这样整合,先创建一个
NSObject
分类,再重写resolveInstanceMethod
方法,然后添加相关处理:
最后为什么要
return NO
而不是[super resolveInstanceMethod:sel]
,是因为苹果底层默认返回的就是NO
。
- 运行结果如下
- 对象方法:
- 类方法:
lookUpImpOrForwardTryCache
当resolveInstanceMethod
和resolveClassMethod
都没有找到时,会走lookUpImpOrForwardTryCache
方法:
IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{
return _lookUpImpTryCache(inst, sel, cls, behavior);
}
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); //behavior为第一次改变后的值
}
IMP imp = cache_getImp(cls, sel);
...
}
复制代码
代码中会再次走lookUpImpOrForward
,此时不会走进那个单例
,直接返回nil
了
总结
-
- 当方法找不到时,都可以使用
resolveInstanceMethod
或者resolveClassMethod
进行监听,并作出相应的补救处理。
- 当方法找不到时,都可以使用
-
- 我们可以利用这个特性,在实际项目中进行一些
bug处理
。
- 我们可以利用这个特性,在实际项目中进行一些
辅助日志
除了动态决议,还有什么办法可以处理呢,显然还是有的,苹果提供了错误日志处理instrumentObjcMessageSends
:
- 这里一个是对
objcMsgLogEnabled
进行赋值,一个是调用_objc_flush_caches
方法进行操作,那么赋值的操作有什么用呢,我们进入看看:
bool objcMsgLogEnabled = false;
static int objcMsgLogFD = -1;
bool logMessageSend(bool isClassMethod,
const char *objectsClass,
const char *implementingClass,
SEL selector)
{
char buf[ 1024 ];
// Create/open the log file
if (objcMsgLogFD == (-1))
{
// 日志文件路径
snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
if (objcMsgLogFD < 0) {
// no log file - disable logging
objcMsgLogEnabled = false;
objcMsgLogFD = -1;
return true;
}
}
// Make the log entry
snprintf(buf, sizeof(buf), "%c %s %s %s\n",
isClassMethod ? '+' : '-',
objectsClass,
implementingClass,
sel_getName(selector));
objcMsgLogLock.lock();
write (objcMsgLogFD, buf, strlen(buf));
objcMsgLogLock.unlock();
// Tell caller to not cache the method
return false;
}
复制代码
- 当
objcMsgLogEnabled
为true
时,会走这个logMessageSend
方法,里面有对日志的存储
我们先删除动态决议的代码
,然后在脱离源码环境
调用instrumentObjcMessageSends
测试下:
extern void instrumentObjcMessageSends(BOOL flag);
// main
instrumentObjcMessageSends(YES);
[teacher sayLike];
instrumentObjcMessageSends(NO);
复制代码
运行结果还是报错,然后cmd + shift + f
进入/tmp/msgSends
路径,找到一个msgSends-64056
文件,然后打开:
- 在文件中,可以看到调用的一些我们熟悉的方法,但出现新方法
forwardingTargetForSelector
和methodSignatureForSelector
方法,这个就是消息转发
中的方法,我们会在下篇文章去探究。