经过上一篇 方法列表的查找(慢速发送流程) 发现我们的调用的方法是没有实现的话,runtime 还给了我们补救的机会,调用了 resolveMethod_locked 方法进行消息转发。接下来这篇内容就是对 消息转发 的探索。
我们新建一个类(Msg_Forward_Class),代码如下:
//Msg_Forward_Class.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Msg_Forward_Class : NSObject
- (void)classInstanceMethod;
+ (void)classClassMethod;
- (void)abc;
@end
NS_ASSUME_NONNULL_END
//Msg_Forward_Class.m
#import "Msg_Forward_Class.h"
@implementation Msg_Forward_Class
//不实现 classInstanceMethod 方法
//不实现 classClassMethod 方法
@end
1、动态方法解析
我们通过源码看到了,如果调用的方法没有实现,那么根据当前 @selector(sel) 是类方法还是对象方法有2个分支,分别处理对象方法和类方法的消息转发。
oc 源码如下:
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);
}
}
//可能调用解析器已经填充了缓存,所以尝试使用它
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
既然苹果分为两个分支处理,那我们也分两步探讨。
- 未实现对象方法:
[cls resolveInstanceMethod:sel] - 未实现类方法:
[nonMetaClass resolveClassMethod:sel]和[cls resolveInstanceMethod:sel]
为什么未实现类方法还调用了 [cls resolveInstanceMethod:sel] 在稍后解析。
动态对象方法转发和类方法转发在主体结构上一样,流程图如下:
1、对象方法消息转发
那么我们点击 resolveInstanceMethod 查看方法实现
/***********************************************************************
* resolveInstanceMethod
* 调用+resolveInstanceMethod,寻找要添加到cls类的方法。
* cls可以是元类也可以是非元类。
* 不检查方法是否已经存在.
**********************************************************************/
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
//...
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.
//缓存结果(好或坏),这样解析器下次就不会触发。
//这个方法是用于查询自己的有没有实现,没有查找父类,一旦找到就插入缓存,包括存入 _objc_msgForward_impcache
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
//...
}
由上方代码中看到,消息转发的核心为下方3行代码:
//消息转发
SEL resolve_sel = @selector(resolveInstanceMethod:);
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
动态方法解析 寻找 resolveInstanceMethod 实现的过程,从当前类一直循环查找父类,直到 NSObject 的父类为 nil 结束。
2、类方法消息转发
去掉一些无用的代码,稍微修改了一下源码,发现,其实类消息转发和对象消息转发代码基本一样,只是对象消息转发是给从类中查找方法,而类消息转发是从元类中查找方法。
查找流程:元类->父元类->根元类->NSObject(元类)->NSObject(类)->nil,其实也就是根据 isa 的流程图进行查找。
resolveClassMethod 源码如下:
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
//...
SEL resolve_sel = @selector(resolveClassMethod:);
if (!lookUpImpOrNilTryCache(inst, resolve_sel, cls)) {
// Resolver not implemented.
return;
}
Class nonmeta;
{
mutex_locker_t lock(runtimeLock);
nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
//...
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(nonmeta, resolve_sel, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
//这个方法是用于查询自己的有没有实现,没有查找父类,一旦找到就插入缓存,包括存入 _objc_msgForward_impcache
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
//...
}
但是在源码中看到,在类消息转发的分支里还调用了 resolveInstanceMethod(inst, sel, cls),这是为什么?
//类方法的消息转发
resolveClassMethod(inst, sel, cls);
//--------- 这里 --------
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
经过最上方动态方法解析的流程图我们基本上了解了 lookUpImpOrNilTryCache 的用途,是为了查找 @selector(sel) 有没有实现(找到就插入缓存)。
上述判断条件为:如果没有实现 @selector(sel) 方法,那么就会调用 resolveInstanceMethod。分析如下:
- 1、操作和编程的只有自己创建的类文件,包括类方法也是写在类文件里,因为元类是编译时确定的,我们无法操作,所以断点会走到类的
.m文件中; - 2、开发中不能直接操作
NSObject类 ,只能实现了NSObject的category,而工程中有的类基本上都由NSObject派生而来 ,所以,这也是苹果给下的一个的接口。 - 3、根据isa 的流程图知道,所有的
meta class最后都会指向NSObject(root class), 因为万物之祖为NSObject(root class)(NSProxy除外!),所以对于NSObject(root class)看是否还调用了对象方法;
如果类消息转发最后也没有实现 [cls resolveInstanceMethod:sel] ,那动态方法解析流程就走完了。
2、快速消息转发
分析过动态方法解析流程之后,把上方代码简化成伪代码:
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
if (不是元类) {
//对象方法的消息转发
} else {
//类方法的消息转发
if (没有找到类方法消息转发的实现) {
//对象方法的消息转发
}
}
//可能调用解析器已经填充了缓存,所以尝试使用它
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
一步步断点,当最后 return lookUpImpOrForwardTryCache(inst, sel, cls, behavior); 后,就回到了 lookUpImpOrForward 方法中:
断点再走一步,我们看到调用了 jmp *%r11 ,这里的 imp = _objc_msgForward_impcache。
继续断点下一步,看到 jmp __objc_msgForward 汇编 ,这里是调用了 __objc_msgForward ,可惜找不到 __objc_msgForward 的对应方法。
然后断点接着走,调用了
1 movq __objc_forward_handler(%rip), %r11
2 jmp *%r11
上方代码第一行就是在赋值 void *_objc_forward_handler = (void*)objc_defaultForwardHandler; 第二行跳转了 r11。
看不到方法了,只能看汇编了,因为这个方法苹果没有开源。既然能断点,那就跟着断点去看看。
这里看到了2个重要的东西。一个是 __forwarding_prep_0___ 和 ___forwarding___,为什么这么说呢?是否记得有方法没有实现 crash 的时候 ,控制台打印的日志?是不是很眼熟?
到这里,重要的东西就来了。既然看到了控制台打印的信息了, 那再看看调用动态消息解析后发生了什么。
进入 call ___forwarding___,汇编如下:
这里看到了将 forwardingTargetForSelector: 方法指针赋值给了 r14 ,下方有 call %r14,截图截少了,抱歉。
call forwardingTargetForSelector:,这个就是 快速消息转发 系统调用。
3、完整消息转发
如果快速消息转发 forwardingTargetForSelector 没有实现的话,汇编继续往下走:
1、方法签名
上述汇编代码就看到了红色的方法注释 methodSignatureForSelector,在 NSObject.h 有方法原型 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector。
2、事务处理
有了方法签名,那还需要对完成方法签名的 SEL sel 判断是否处理,此时就需要用到 - (void)forwardInvocation:(NSInvocation *)invocation 方法了。可以选择不处理完成方法签名的 SEL sel,但是必须实现 forwardInvocation: 方法。
4、最后 crash 的地方
如果上述方法都没有完成对未实现方法调用的补救,那么就会 crash,从汇编中看,会调用下面这个方法:
NSObject.m 中实现如下:
// Replaced by CF (throws an NSException)
- (void)doesNotRecognizeSelector:(SEL)sel {
_objc_fatal("-[%s %s]: unrecognized selector sent to instance %p",
object_getClassName(self), sel_getName(sel), self);
}
上面这个太眼熟了对吧,crash 后控制台经常打印。
5、总结
到此,消息转发的流程就全部完成了。补一个控制台的 crash 的队栈信息:
看了上面的流程再看这个,是不是一目了然,是不是对这个 crash 信息再也不迷茫了!。
下方是消息转发的流程图:
5、关于 resolveInstanceMethod 调用2次的问题
在控制台上,发现动态消息转发打印了2次,其他都是一次。
现在把代码流程全部补全:
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSLog(@"%s %@ 来了",__FUNCTION__, NSStringFromSelector(sel));
return [super resolveInstanceMethod:sel];
}
- (id)forwardingTargetForSelector:(SEL)aSelector
{
NSLog(@"%s %@ 来了",__FUNCTION__, NSStringFromSelector(aSelector));
return [super forwardingTargetForSelector:aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSLog(@"%s %@ 来了",__FUNCTION__, NSStringFromSelector(aSelector));
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
NSLog(@"%s %@ 来了",__FUNCTION__, NSStringFromSelector(anInvocation.selector));
return [super forwardInvocation:anInvocation];
}
再看控制台打印:
上面控制台第二次 resolveInstanceMethod 方法是在 methodSignatureForSelector 之后打印,看不到源码,所以开始汇编下手。
继续断点:
最后断点来到了这里,目前猜测是 methodSignatureForSelector 后,去匹配一下方法 imp 实现。
6、结尾
到此消息转发的内容就结束了,发消息这里分了3篇文章去写了。
下一篇:类的加载。
PS:可以运行的并且不断进行注释的objc 源码地址。