在上篇文章objc_msgSend探索,我们探索了正常的方法的快速查找
和方法的慢速查找
流程,同时我们也知道未找到imp时,会将最初定义的消息转发的imp赋值给当前imp,进行消息转发流程.今天我们就来探索一下这个消息转发流程
.
_objc_msgForward
我们在上篇文章,知道如果找不到方法,会将imp赋值成_objc_msgForward_impcache
,我们在源码的objc-msg-arm64.s
文件中查找其汇编写的实现发现其就是调用_objc_msgForward
,_objc_msgForward
中三行代码,两行代码是在调用_objc_forward_handler
STATIC_ENTRY __objc_msgForward_impcache
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
复制代码
我们在objc-runtime.m
中查找到
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;
void objc_setForwardHandler(void *fwd, void *fwd_stret){
_objc_forward_handler = fwd;
#if SUPPORT_STRET
_objc_forward_stret_handler = fwd_stret;
#endif
}
复制代码
_objc_forward_handler
默认赋值的是我们熟悉的方法崩溃信息,并且下面有一个objc_setForwardHandler
函数对其进行重新赋值.我们也看到了类方法、实例方法的崩溃信息是放在一起的,只是根据class_isMetaClass
来进行判断,再次证明objc底层中并没有类方法和实例方法的区别. 上面都是这个找不到方法的imp的赋值,并没有进行调用,在lookUpImpOrForward
方法中for循环赋值imp下面,我们也会找到一段代码,其只会执行一次
// No implementation found. Try method resolver once.
// 第一次进入时候传入的behavior为LOOKUP_INITIALIZE|LOOKUP_RESOLVER (0001 | 0010 = 0011)
// 0011 & 0010 = 0010 条件成立,会进入,然后重置behavior为LOOKUP_INITIALIZE 0001
// 再次调用 0001 & 0010 = 0000 则不会进入
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
复制代码
resolveMethod_locked
当所有查找方式没有找到目标imp
时,系统会调用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()) { // 判断是否是元类
// 不是元类,调用resolveInstanceMethod方法
resolveInstanceMethod(inst, sel, cls);
}
else {
// 是元类,调用resolveClassMethod
resolveClassMethod(inst, sel, cls);
// 如果调用上面的方法还没有找到,尝试调用resolveInstanceMethod,
// 原因是根据isa的继承链,根元类的父类是NSObject,所以在元类中如果没有找到,最后可能会在NSObjct中找到目标方法
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
// 重新调用lookUpImpOrForwardTryCache方法,返回方法查找流程,因为已经进行过一次动态方法决议,下次将不会再进入,所以不会造成死循环
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
复制代码
很简单的一个方法,里面实际上就是判断我们传入的类
是否是元类
,然后再调用不同的方法,我们先看看返回的lookUpImpOrForwardTryCache
,其就是调用的_lookUpImpTryCache
方法,只是传递的behavior = 4
.我们要注意一点,元类
在调用resolveClassMethod
后如果没有找到方法,会再次尝试调用resolveInstanceMethod
.
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior){
runtimeLock.assertUnlocked();
if (slowpath(!cls->isInitialized())) {
// 判断类没有初始化,重新调用 lookUpImpOrForward 进行查找
return lookUpImpOrForward(inst, sel, cls, behavior);
}
IMP imp = cache_getImp(cls, sel); // 又一次从imp缓存中查找是否存在
if (imp != NULL) goto done; // 找到了则跳去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)) { // imp没找到, 再次调用lookUpImpOrForward, behavior = 1,不会再次调用动态方法决议
return lookUpImpOrForward(inst, sel, cls, behavior);
}
done:
if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
return nil; // 说明动态方法决议已经执行过,且没有添加imp,imp == 消息转发imp, 则直接返回空
}
return imp; // 说明动态方法决议中添加了对应的imp
}
复制代码
其就是给我们的一次补救的机会,最后还是会再次调用方法查找流程,并且再次调用的过程中不会再次调用动态方法决议
流程,避免死循环.消息转发的实现重点是在上面的现在我们来研究一下resolveInstanceMethod
和resolveClassMethod
resolveInstanceMethod方法
首先我们看看实例方法的动态决议resolveInstanceMethod
static void resolveInstanceMethod(id inst, SEL sel, Class cls){
……
SEL resolve_sel = @selector(resolveInstanceMethod:); // 创建一个方法名为resolveInstanceMethod的SEL
// 判断resolveInstanceMethod是否在目标类中实现
if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel); // 通过objc_msgSend方法调用resolveInstanceMethod, 消息接收者是cls,也就是说明 resolveInstanceMethod 是类方法
// 缓存结果,不会调用第二次,重新调用方法查找流程
// 虽然调用了resolveInstanceMethod,但是这个返回值是bool
// 所以我们要获取对应的imp,还是需要通过方法查找流程
// 如果通过resolveInstanceMethod添加了方法,就缓存在类中
// 没添加,则缓存forward_imp
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
if (resolved && PrintResolving) { // 组装日志并打印日志
……
}
}
复制代码
这个方法也相对比较简单,最后调用lookUpImpOrNilTryCache
,其内部仍然是调用上面的_lookUpImpTryCache
返回方法查找的流程.总结如下:
- 首先创建一个方法名为
resolveInstanceMethod
的SEL
对象resolve_sel
; - 然后判断
resolve_sel
是否实现,这里通过cls->ISA
可以知道,resolveInstanceMethod
是个类方法
; - 通过
objc_ msgSend
直接调用resolveInstanceMethod
方法,因为是直接对cls
发送消息,所以也可以看出resolveInstanceMethods
是类方法
; - 调用
lookUpImpOrNilTryCache
方法,重新返回到方法查找的流程当中去;
resolveClassMethod
我们再来看看类方法的流程
static void resolveClassMethod(id inst, SEL sel, Class cls){
……
// 判断resolveClassMethod是否在目标类中实现
if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
return;
}
Class nonmeta; // 获取目标类
{
mutex_locker_t lock(runtimeLock);
nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
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); // 通过objc_msgSend调用类中的resolveClassMethod方法
// 缓存结果,不会调用第二次,重新调用方法查找流程
// 类方法实际上就是元类对象中的对象方法,所以可以通过方法查找流程在元类中查找
// 如果通过resolveClassMethod添加了,就缓存方法在元类中
// 没添加,则缓存forward_imp
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
if (resolved && PrintResolving) {// 组装日志并打印日志
……
}
}
复制代码
这个方法与resolveInstanceMethod
比较类似,如果通过resolveClassMethod
方法添加了目标imp
,则将其缓存在目标元类
中,否则缓存forward_imp
。
动态决议验证
我们跑一个demo来确认其是否调用 我们可以看到,resolveInstanceMethod
方法执行了两次,为什么会进行两次呢?我们查看崩溃日志 发现其还调用了__forwarding__
,这个我们在后面再看,当我们在resolveInstanceMethod
中实现imp,则其就会调用实现的imp,类方法resolveClassMethod
也是一样 如果当我们在resolveClassMethod
中的方法动态决议的实例方法也没有实现,其会调用resolveInstanceMethod
的方法动态决议,如果我们在resolveClassMethod
中的方法动态决议的使用的是元类的方法,则会死循环,一直查找元类该方法,因为resolveMethod_locked
中根据! cls->isMetaClass()
判断调用哪个方法
在NSObject中实现resolveInstanceMethod
之前我们都探索过,无论是类还是元类,其根类都是NSObject
,实例方法和类方法在慢速查找过程中如果找不到最后都会查找NSObject
的方法,那我们对NSObject建个分类NSObject+ResolveInstanceMethod
,然后在其中实现resloveInstanceMethod
方法,则即可功的避免崩溃,虽然可行,只需了解,并不推荐,因为运行时系统会调用很多自己的方法,NSObject
分类中重写resloveInstanceMethod
方法会导致无限调用,出现崩溃。 resolveInstanceMethod
只是我们防止崩溃的手段,并不能解决我们实际的问题。他的原理很简单:
- 如果是实例方法,那么根据
isa的继承链
最后会在NSObject
中找到resolveInstanceMethod
方法并调用,因此我们在NSObject
中动态添加方法是可行的。 - 如果是类方法,由于
根元类
的父类是NSObject
的原因,所以还会调用一次resolveInstanceMethod
,最后在NSObject
中找到并执行。
我们上篇文章了解到,查找到方法后会调用log_and_fill_cache
写入cache中,该方法不仅可以写入cache,其还有个判断写入objcMsgLogEnabled
来写入日志,我们在源码中全局搜索,发现仅有一个地方赋值为void instrumentObjcMessageSends(BOOL flag)
,我们来看一下写入的日志信息
extern void instrumentObjcMessageSends(BOOL flag);
WTPerson *p = [WTPerson new];
instrumentObjcMessageSends(true);
[p run];
instrumentObjcMessageSends(false);
复制代码
我们看到,在动态决议方法之后,崩溃之前,还调用了forwardingTargetForSelector
和methodSignatureForSelector
两个方法, 这两个方法就是消息的转发
,这三个方法就是我们常说的三根救命稻草
。
消息的转发 - forwardingTargetForSelector
还是之前的demo,我们不在实现resloveInstanceMethod
,而是使用forwardingTargetForSelector
我们通过forwardingTargetForSelector
将方法转发给可以实现方法的类,这个就是消息的快速转发.当前方法的cache也是缓存到WTStudent
中,因为已经转发给WTStudent
了
消息的转发 - methodSignatureForSelector
当快速转发也无法成功的时候,就剩下最后从拯救机会,methodSignatureForSelector
也可以写在NSObject
中和resloveInstanceMethod
一样防止崩溃,他本身只是发送信号,需要和另一个接收信号的方法forwardInvocation
一起使用。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"%s --> %@", __func__, NSStringFromSelector(aSelector));
return [NSMethodSignature signatureWithObjCTypes:"v@::"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"%s --> %@", __func__, anInvocation);
WTStudent *stu = [WTStudent new];
if ([self respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:self];
} else if([stu respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:stu ];
} else {
NSLog(@"别调这个方法了,尝试其他方法试试");
SEL originSel = anInvocation.selector;
anInvocation.target = stu;
anInvocation.selector = @selector(runslow:);
//**0位置是targe 1位置是sel,所以自定义的参数传递只能从2开始**
[anInvocation setArgument:&originSel atIndex:2];
[anInvocation invoke];
}
[stu runslow];
}
复制代码
上面提到resloveInstanceMethod
调用了两次,现在我们来弄明白问什么。我们先打印两次的一下堆栈信息。 第一次进入是objc_msgSend_uncached
未找到cache时来到了我们的resloveInstanceMethod
,第二次进入是CF_forwarding_prep_0
,__forwarding__
进行消息转发methodSignatureForSelector
, methodSignatureForSelector
->class_getInstanceMethod
->lookUpImpOrForward
->resloveInstanceMethod
。
总结
防止崩溃的三根救命稻草
:resloveInstanceMethod
、forwardingTargetForSelector
和methodSignatureForSelector
resloveInstanceMethod
消息的动态决议流程:
- 首先判断
类
是否初始化,如果没有初始化则直接调用lookUpImpOrForward
,里面有针对没初始化的类
进行相应的处理; - 然后去缓存中进行
方法的快速查找
,找到了就去done
- 缓存中没找到,如果支持共享缓存,则去共享缓存中查找
- 都没有查找到,则通过慢速方法查找去查找方法,
behavior = 4
,这次慢速查找不会再次调用动态方法决议
- 在
done
流程中,如果已经执行过动态方法决议
且并没有添加imp
,则缓存中的sel
对应imp
为消息转发imp
,这时直接返回nil
。否则返回添加的imp
实现。
当我们动态决议中没有处理,则可以使用forwardingTargetForSelector
进行快速消息转发,但是这种方式调用的方法名和参数必须完全一样,比较死板。如果转发的对象也无法实现对应方法,则使用最后的methodSignatureForSelector
发送信号,进入慢速消息转发流程,来动态的添加方法的实现,如果三次都无法处理,则触发doesNotRecognizeSelector
崩溃。