前言
前文介绍的方法的查找流程:快速查找和慢速慢速查找流程。但是并没有讲当方法实现没有找到时,系统是如何处理的,本文将介绍这一部分。
1. 方法决议
if (slowpath(behavior & LOOKUP_RESOLVER)) {
//LOOKUP_RESOLVER = 2 behavior = 3
//behavior 异或为1
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
复制代码
当慢速查找没有找到时,进入方法决议阶段——resolveMethod_locked
。
在进入resolveMethod_locked
查看方法决议过程前,先提前了解几个重要方法的实现和作用。
1.1 resolveInstanceMethod:和 resolveClassMethod:
NSObject.h
+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
NSObject.mm
+ (BOOL)resolveClassMethod:(SEL)sel {
return NO;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;
}
复制代码
resolveClassMethod:
为类方法的给定选择器提供动态实现。
resolveInstanceMethod:
为实例方法的给定选择器提供动态实现。
这两个类方法在基类NSObject
中声明和实现,目的是当在慢速查找imp
时,如果没有找到imp
,可以当前的sel
动态的添加方法实现。默认返回NO
。
子类可以重写这个方法,为指定的sel
添加动态实现,然后返回YES
,表示进行了这个方法的动态决议。
以官方提供的例子为例:
Objective-C
方法只是一个 C
函数,它至少接受两个参数——self和_cmd。使用该函数,可以将函数作为方法添加到类中。
void dynamicMethodIMP(id self, SEL _cmd)
{
// implementation ....
}
复制代码
给定以下函数:class_addMethod
,可以使用它动态地将dynamicMethodIMP
作为方法(称为)添加到类中,如下所示:resolveInstanceMethod:resolveThisMethodDynamically
+ (BOOL) resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically))
{
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSel];
}
复制代码
1.2 resolveInstanceMethod/resolveClassMethod
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
//注册resolveInstanceMethod:方法签名 为调用方法准备
//resolveInstanceMethod: 是一个类方法, 可在自定义实现, 进行方法的决议
//
SEL resolve_sel = @selector(resolveInstanceMethod:);
//判断resolveInstanceMethod是否已实现 没有就直接return
if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
// Resolver not implemented.
return;
}
//调用resolveInstanceMethod 方法
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
// 查找sel的imp, imp可能为空
// lookUpImpOrNilTryCache执行过程中会调用lookUpImpOrForward,
// 如果经过方法决议后, 有了sel的方法实现imp, 经过查找, 会将imp 存入缓存, 以后方法决议就不会再触发了
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
主要分为两部分,调用resolveInstanceMethod:
方法进行方法决议,然后进行一次imp
查找。
lookUpImpOrNilTryCache
可能会返回空值 nil。这里执行结果imp, 并不会返回,只是输出记录的时候使用一下。但是找到的imp
(不管是真正的方法实现,还是forward_imp
)会被缓存起来,后续在查找时,直接在缓存中就可以找到了。
有时候resolveInstanceMethod:
可能返回YES,但是并没有真正的添加方法实现。所以经过一次查找之后,可以最终确认一下。然后将决议结果记录输出。
resolveClassMethod
参照resolveInstanceMethod
即可。
1.3 lookUpImpOrNilTryCache/lookUpImpOrForwardTryCache
lookUpImpOrNilTryCache
, 查找imp, 找不到会返回nil。behavior | LOOKUP_NIL
使得behavior & LOOKUP_NIL
结果为真。具体判断参见lookUpImpOrForward
的结尾部分。
IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
{
//behavior | LOOKUP_NIL = 7
return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
}
复制代码
lookUpImpOrForwardTryCache
,查找imp, 找不回会返回一个默认的方法实现 _objc_msgForward_impcache
。
IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{
return _lookUpImpTryCache(inst, sel, cls, behavior);
}
复制代码
1.4 _lookUpImpTryCache
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
// behavior = 1
runtimeLock.assertUnlocked();
if (slowpath(!cls->isInitialized())) {
// see comment in lookUpImpOrForward
// cls未初始化, 去lookUpImpOrForward查找sel的imp,
// 此时behavior = 1 不会再进行方法决议
return lookUpImpOrForward(inst, sel, cls, behavior);
}
//从缓存中查找 快速查找
IMP imp = cache_getImp(cls, sel);
//imp 不为空 直接返回
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为空, 进行慢速查找 并将结果返回
if (slowpath(imp == NULL)) {
return lookUpImpOrForward(inst, sel, cls, behavior);
}
//返回imp 或 nil
done:
if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
return nil;
}
return imp;
}
复制代码
_lookUpImpTryCache
是一个标准的imp
查找方法。先去查找缓存cache_getImp
,找不到就去进行慢速查找lookUpImpOrForward
。
注意,如果是在方法慢速查找过程中执行_lookUpImpTryCache
,说明第一遍的慢速查找没有找到,进行了方法决议,方法决议后再次查找。如果执行到lookUpImpOrForward
,这已经是第二次执行它了,behavior
已经与LOOKUP_RESOLVER(值为2)执行过异或了, 与LOOKUP_RESOLVER(2)做与运算不会再为真了,所以不可能在进行一次方法决议了。
1.5 resolveMethod_locked
经过前面几个方法介绍,再来看resolveMethod_locked
方法决议流程就清楚多了。
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
//behavior=1
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
if (! cls->isMetaClass()) {
//cls不是元类 inst为实例对象 sel为实例方法
//进行实例方法决议
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else {
//cls是元类 inst是类对象 sel是类方法
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
//先进行类方法决议
resolveClassMethod(inst, sel, cls);
//调用lookUpImpOrNilTryCache 判断经过类方法决议之后, sel是否已经有了对应的imp
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
//如果仍然没有找到imp, 再进行一次实例方法决议
//为什么可以再进行一次实例方法决议, 通过isa走位图, 根元类的父类是 NSObject类, NSObject类里存储的实例方法, 所以可以再进行一次实例方法决议.
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
// 进行方法决议之后, 可能为sel添加了方法实现imp, 因此再进行一次方法查找
// 此时的behavior=1 即使还找到imp也不会再进行方法决议了, 而是返回imp(可能不是真正的imp, 而是_objc_msgForward_impcache)
// 将结果返回
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
复制代码
resolveMethod_locked
会对当前的cls
进行判断,执行不同的方法决议流程。
如果cls
不是元类,那么当前inst
为实例对象 sel
为实例方法,执行实例方法决议流程。
如果cls
是元类,那么当前inst
是类对象 sel
是类方法,执行类方法决议流程:
- 先进行类方法决议
resolveClassMethod
- 然后查找
imp
, 判断经过类方法决议之后, sel是否已经有了对应的imp - 如果仍然没有找到imp, 再进行一次实例方法决议
为什么可以再进行一次实例方法决议?
通过isa走位图可以, 根元类的父类是 NSObject类。 NSObject类里存储的实例方法, 加入在NSObject的实例决议方法,所以可以再进行一次实例方法决议.
最后,会调用lookUpImpOrForwardTryCache
,进行imp
的查找并将结果返回(可能是真正的imp, 也可能是_objc_msgForward_impcache
)。
我们已经知道在前面执行
resolveInstanceMethod(inst, sel, cls);
复制代码
或者
resolveClassMethod(inst, sel, cls);
复制代码
时,方法内部已经进行了一次imp查找。这次执行lookUpImpOrForwardTryCache
在缓存中就会找到imp
了。
1.6 举例
说了这么多,举个例子测试一下。
定义ZPerson
类,声明sayHello
和sayNB
方法,但是只实现了sayNB
方法。
同时,重写方法resolveInstanceMethod
,添加打印,以确认找不到imp
时,会进入方法决议。
@interface ZPerson : NSObject
- (void)sayHello;
- (void)sayNB;
@end
@implementation ZPerson
//- (void)sayHello{
// NSLog(@"你好");
//}
- (void)sayNB{
NSLog(@"你牛逼");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"进入实例方法动态决议");
return [super resolveInstanceMethod:sel];
}
@end
复制代码
创建ZPerson
实例,并执行sayHello
方法。由于没有提供sayHello
的实现和+resolveInstanceMethod:
的实现,不出意外会崩溃。
ZPerson *person = [ZPerson alloc];
[person sayHello];
复制代码
执行结果:
不出所料,由于没有找到真正的方法实现,程序崩溃了(2)。
同时在输出错误信息之前,执行了+resolveInstanceMethod:
(1)。
但是我们注意到
+resolveInstanceMethod:
执行了两次,这是为什么呢,我们将在后面介绍。
下面我们在+resolveInstanceMethod:
方法中为sayHello
添加动态方法实现:
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(sayHello)) {
NSLog(@"执行 sayHello 方法决议");
IMP sayHelloIMP = class_getMethodImplementation(self, @selector(sayNB));
Method sayHelloMethod = class_getInstanceMethod(self, @selector(sayNB));
const char *sayHelloType = method_getTypeEncoding(sayHelloMethod);
return class_addMethod(self, sel, sayHelloIMP, sayHelloType);
}
return [super resolveInstanceMethod:sel];
}
复制代码
在决议方法中,动态为sayHello
提供了实现,不会崩溃了。会转而执行sayNB
方法。
这次不崩溃了。
对于崩溃的一点思考:
既然可以在resolveInstanceMethod:
中为方法添加动态实现,那么以后是不是就可以避免此类问题产生的崩溃了呢?这是可以的。我们可以在NSObject
的分类中重写此方法,来统一处理。而且不管是实例方法
还是类方法
,都可以在resolveInstanceMethod:
中动态添加实现。
但是也有一些缺陷。当系统方法可能会被更改,可以通过为自定义方法添加前缀来解决。这种方式的侵入性比较强,如果子类中也重写了此方法,就不会执行NSObject
的方法了。
2. 消息转发
2.1 objcMsgLogEnabled
继续思考前面提出的问题,当在resolveInstanceMethod:
中没有正确处理sel
时,会执行两次该方法,但是我们完整看完整个方法决议的过程,并没有找到第二次执行方法决议时机。我们猜测可能是在系统处理崩前,做了一些操作。
查看崩溃堆栈:
我们发现,从main
中出现异常,到动态库抛出异常objc_exception_throw + 48
,中间经过了CoreFoundation
库的处理。但是CoreFoundation
是非开源的,我们暂时还不能知道具体发生了啥。
但是我们在前面方法慢速查找的过程中,知道可以输出查找imp
的日志到文件中,我们试试,能不能知道一些线索。
instrumentObjcMessageSends(true);
[person sayHello];
复制代码
在调用sayHello
之前,打开objcMsgLogEnabled
。
然后对应日志文件,查看。
+ ZPerson NSObject resolveInstanceMethod:
+ ZPerson NSObject resolveInstanceMethod:
- ZPerson NSObject forwardingTargetForSelector:
- ZPerson NSObject forwardingTargetForSelector:
- ZPerson NSObject methodSignatureForSelector:
- ZPerson NSObject methodSignatureForSelector:
+ ZPerson NSObject resolveInstanceMethod:
+ ZPerson NSObject resolveInstanceMethod:
- ZPerson NSObject doesNotRecognizeSelector:
- ZPerson NSObject doesNotRecognizeSelector:
复制代码
找到如下方法:
resolveInstanceMethod:
forwardingTargetForSelector:
methodSignatureForSelector:
doesNotRecognizeSelector:
之所以都是两个,是因为这些方法在执行前都会查找方法存在不存在,执行慢速查找时,记录一次。由于不会缓存,所以真正执行时,又执行一次慢速查找,又记录一次。
我们看到在methodSignatureForSelector:
之后,doesNotRecognizeSelector:
方法之前,又执行了一次方法决议resolveInstanceMethod:
。是不是可以猜测吗,系统报错之前,苹果又给了一次机会,进行方法查找呢?
2.2 forwardingTargetForSelector
搜索forwardingTargetForSelector
发现只在NSObject
中提供了定义和默认实现,没有找到更多信息。
但是没事,既然是OC
方法,我们先去官方文档找一找。
通过文档,大概知道此方法可返回一个对象,此对象用来接收传入的方法。也可以返回nil,但是不能返回自身(self
)那样会陷入死循环。
如果只想将消息重定向到另一个对象时,可以使用此方法,并且可以比常规转发快一个数量级。如果消息转发的目的是捕获 NSInvocation
或在转发期间操作参数或返回值,就不能使用它了。可以使用forwardInvocation:
。
我们尝试使用一下:
定义ZStudent
类,并实现sayHello
方法。
@interface ZStudent : NSObject
- (void)sayHello;
@end
- (void)sayHello{
NSLog(@"学生说: 你好");
}
复制代码
在ZPerson
类中重写forwardingTargetForSelector:
方法,将sayHello
消息,交给一个ZStudent
对象处理。
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(sayHello)) {
return [ZStudent alloc];
}
return [super forwardingTargetForSelector:aSelector];
}
复制代码
执行结果:
进入实例方法动态决议
学生说: 你好
复制代码
程序没有崩溃,也在进入方法的快速转发之前,执行了一次方法决议。
这就是消息的快速转发,可以将当前对象不能处理的消息,交给一个能处理这个消息的对象来处理。
如果没有实现forwardingTargetForSelector
方法快速转发或者没有处理传入SEL
,系统将进入慢速消息转发流程。
2.3 forwardInvocation
When an object is sent a message for which it has no corresponding method, the runtime system gives the receiver an opportunity to delegate the message to another receiver. It delegates the message by creating an NSInvocation object representing the message and sending the receiver a forwardInvocation: message containing this NSInvocation object as the argument. The receiver’s forwardInvocation: method can then choose to forward the message to another object. (If that object can’t respond to the message either, it too will be given a chance to forward it.)
The forwardInvocation: message thus allows an object to establish relationships with other objects that will, for certain messages, act on its behalf. The forwarding object is, in a sense, able to “inherit” some of the characteristics of the object it forwards the message to.
复制代码
当一个对象收到一条没有相应方法实现的消息时,运行时系统会给接收者一个机会——将消息委托给另一个接收者处理。它通过创建一个NSInvocation
对象来包装需要委托的消息,NSInvocation
包含一个消息接收者target
,一个方法选择器sel
,消息的参数和返回值。NSInvocation
对象作为参数,传入接收者的forwardInvocation
方法,来将将消息转发给另一个对象。(如果该对象也无法响应消息,它也将有机会转发它。)
就像下面这样:
- (void)forwardInvocation:(NSInvocation *)invocation
{
SEL aSelector = [invocation selector];
if ([friend respondsToSelector:aSelector])
[invocation invokeWithTarget:friend];
else
[super forwardInvocation:invocation];
}
复制代码
注意1:
Important
To respond to methods that your object does not itself recognize, you must override methodSignatureForSelector: in addition to forwardInvocation:. The mechanism for forwarding messages uses information obtained from methodSignatureForSelector: to create the NSInvocation object to be forwarded. Your overriding method must provide an appropriate method signature for the given selector, either by pre formulating one or by asking another object for one.
复制代码
除了实现forwardInvocation:
方法之外,还需要重写methodSignatureForSelector:
方法。系统要想获得创建NSInvocation
对象需要的信息,需要methodSignatureForSelector:
方法为给定的方法选择器SEL
提供一个方法签名。
methodSignatureForSelector:
也可以返回nil
,这样就不会执行forwardInvocation:
了。
注意2:
NSObject’s implementation of forwardInvocation: simply invokes the doesNotRecognizeSelector: method; it doesn’t forward any messages. Thus, if you choose not to implement forwardInvocation:, sending unrecognized messages to objects will raise exceptions.
复制代码
NSObject
提供的默认实现只是调用方法;它不转发任何消息。因此,如果您选择不实现forwardInvocation:
,向对象发送无法识别的消息将引发异常,从而会执行doesNotRecognizeSelector:
方法。
下面看一下慢速消息转发的实现流程。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"methodSignatureForSelector:");
if (aSelector == @selector(sayHello)) {
NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
return methodSignature;
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"forwardInvocation: %@", anInvocation);
if (anInvocation.selector == @selector(sayHello)) {
[anInvocation invokeWithTarget:[ZStudent alloc]];
} else {
[super forwardInvocation:anInvocation];
}
}
复制代码
结果:
进入实例方法动态决议
methodSignatureForSelector:
进入实例方法动态决议
forwardInvocation: <NSInvocation: 0x10053bf20>
学生说: 你好
复制代码
将ZPSerson
的sayHello
方法转发给了ZStudent
执行。
同时我们看到,在消息转发之前,方法签名methodSignatureForSelector:
之后,进行消息转发forwardInvocation:
之前,执行了一次方法决议。我们知道方法决议是发生在方法查找过程中的。在进行方法签名之后,将进行消息转发最后的步骤了,也是最后的机会了。如果返回签名进入转发阶段,如果返回nil
崩溃报错。 如果我们可以猜测,在系统进行方法签名后,系统又进行了一次方法查找,所以会执行两次方法决议。第一次发生在方法决议时,第二次发生在方法签名之后。
3.总结
当试图调用没有没有实现的方法时,会进入一下流程:
resolveInstanceMethod
:方法决议,为发送消息的对象的添加一个动态添加一个IMP
,然后再执行forwardingTargetForSelector
:快速转发,将该消息直接转发给一个能处理该消息的对象methodSignatureForSelector
和forwardInvocation
:第一个方法生成方法签名,然后创建NSInvocation
对象作为参数给第二个方法,然后在第二个方法里面做消息处理,只要在第二个方法里面不执行父类的方法,即使不处理也不会崩溃- 以上三步,在任何一步正确处理,都不会产生崩溃,否则将报错崩溃