前文提要
iOS 底层探索文章系列
1、动态方法决议
1.1 消息发送
我们先简单回顾一下消息发送流程
- 先去本类的缓存方法列表中查找
- 如果没有找到,就去本类的方法列表中查找
- 如果当前方法列表还是没有,就通过
superClass
指针在继承链中一直向上循环去查找,一直找到根NSObject
。 - 如果还是没有找到,那么就进入 消息转发流程。
- 到了消息转发流程还是没有处理的话,那么就会报
unrecognized selector
错误。
1.2 动态方法决议
当一个方法没有实现时,也就是在 cache list
和其继承关系的 method list
中,没有找到对应的方法。这时会进入消息转发阶段,但是在进入消息转发阶段前,Runtime
会给一次机会动态添加方法实现。
我们可以通过重写 resolveInstanceMethod
和 resolveClassMethod
方法,动态添加未实现的方法。
其中第一个是添加实例方法,第二个是添加类方法。这两个方法都有一个 BOOL
返回值,返回 NO
则进入消息转发流程。
1.2.1 底层方法实现
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 (!lookUpImpOrNil(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
这里就是我们需要研究的动态方法解析代码了。流程如下
- 如果我们调用的是实例方法,那么
cls
就不是元类,就会执行实例方法的动态决议。 - 如果我们调用的是类方法,那么
cls
就是元类,则会先调用类方法的动态解析。如果没有找到,我们还会调用实例方法的动态解析。这是调用元类的实例方法,根据继承链,会从根元类(元类的isa
会指向根元类)开始找,最终会找到NSObject
根类的resolveInstanceMethod
方法。
注意:类方法存储在元类里面是实例方法。
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
// 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.
// +resolveInstanceMethod adds to self a.k.a. cls
IMP imp = lookUpImpOrNil(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
和 resolveClassMethod
都会看 cls
是否实现了对应的方法,然后给这个类发送消息,如果动态决议提供了方法,那么下次 lookUpImpOrNil
就会命中。resolveMethod_locked
最后就会返回对应的 IMP
。
1.2.2 示例代码
下面是 resolveInstanceMethod
实例代码,我们调用了没有实现的 sayBad
对象方法。
- (void)zl_sayBad {
// implementation
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(sayBad)) {
NSLog(@"来了老弟 %@", NSStringFromSelector(sel));
IMP imp = class_getMethodImplementation(self, @selector(zl_sayBad));
Method method = class_getInstanceMethod(self, @selector(zl_sayBad));
const char *type = method_getTypeEncoding(method);
return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod:sel];
}
我们可以在 resolveInstanceMethod
处下一个断点
我们可以看到首先方法没有被缓存,调用了 _objc_msgSend_uncached
函数,之后查找 IMP
或者转发 lookUpImpOrForward
,然后来到 resolveInstanceMethod
。这是发生在消息转发之前的,在执行完 class_addMethod
并返回 YES
之后,就把 SEL
和 IMP
添加到类里面了,同时会进行缓存方法。我们可以连续调用两次 sayBad
方法。
可以看到,resolveInstanceMethod
只调用了一次。
下面我们看一下 resolveClassMethod
实例代码,我们调用了没有实现的 sayHappy
类方法。
+ (void)zl_sayHappy {
// implementation
}
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(sayHappy)) {
NSLog(@"来了老弟 %@", NSStringFromSelector(sel));
IMP imp = class_getMethodImplementation(objc_getMetaClass("ZLStudent"), @selector(zl_sayHappy));
Method method = class_getInstanceMethod(objc_getMetaClass("ZLStudent"), @selector(zl_sayHappy));
const char *type = method_getTypeEncoding(method);
return class_addMethod(objc_getMetaClass("ZLStudent"), sel, imp, type);
}
return [super resolveClassMethod:sel];
}
其实根据继承链路图,我们可以在根类 NSObject
里面实现 resolveInstanceMethod
进行重写方法,来进行统一拦截。
2、消息转发
2.1 探究底层源码
如果 IMP
没有找到,并且动态方法决议阶段也没有处理,则进入消息转发阶段
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
这部分又回到了汇编代码 __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
下面我们又回调了C函数部分 _objc_forward_handler
__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;
这就是我们开发中最常看到的 unrecognized selector sent to instance 了。但是我们怎么没有看到消息转发部分的源码呢?
其实是因为苹果没有把它开源,所以没法看,但是我们怎么知道中间有哪些过程呢?其实我们可以开一下上帝视角。
// 内部一个打印消息的函数
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
ZLStudent *student = [ZLStudent alloc];
instrumentObjcMessageSends(YES);
[student sayBad];
instrumentObjcMessageSends(NO);
}
return 0;
}
这个函数开启之后,会在 /private/tmp
目录下创建一个 msgSends-xxxx
,xxxx
是内部生成的一个编号。
然后就可以看到调用里面对对象进行了一些的函数调用过程
resolveInstanceMethod
forwardingTargetForSelector
methodSignatureForSelector
resolveInstanceMethod
doesNotRecognizeSelector
这时候,了解消息转发流程的同学可能要问了,forwardInvocation
怎么没看到?其实这是因为我们没有调用 methodSignatureForSelector
,所以这时候是不会调用 forwardInvocation
的。我们现在加上这段
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"来了老弟 %s", __FUNCTION__);
if (aSelector == @selector(sayBad)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
然后我们重新查看新生成的日志,就能看到调用了 forwardInvocation
了。
消息转发流程也可以分为两个阶段
- 快速消息转发
- 慢速消息转发
我们接下来先看一下快速消息转发的实现流程
2.2 快速消息转发
我们可以在 forwardingTargetForSelector
方法中将未实现的消息,转发给其他对象。可以在下面的示例代码中看到,返回响应未实现方法的其他对象。
可以看到,消息接收者成功被转到 ZLPicker
对象身上去了。但是如果 forwardingTargetForSelector
方法未做出任何响应的话,就会来到 消息慢速转发流程 上了。
2.3 慢速消息转发
慢速消息转发时,首先会调用 methodSignatureForSelector
方法,在方法内部生成 NSMethodSignature
类型的方法签名对象。在生成签名对象时,可以指定 target
和 SEL
,可以将这两个参数换成其他参数,将消息转发给其他对象。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"来了老弟 %s", __FUNCTION__);
if (aSelector == @selector(sayBad)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
生成 NSMethodSignature
签名对象后,就会调用 forwardInvocation
方法,这是消息转发中最后一步了。在这一步,只要我们重写了 forwardInvocation
方法,就算不做任何操作,也不会发送消息找不到的崩溃了,只是这样会造成 事务 的浪费。
如果我们实在找不到其他对象进行处理,那么我们可以就像漂流瓶一样把 anInvocation
放出去。
这种方法是因为我们开了上帝视角才找到系统调用了这些方法的,那么有没有一些让我们实实在在看到系统的调用流程呢,接下来我们使用 Hopper Disassembler 反汇编来分析一下消息转发调用流程。
3、Hopper Disassembler 分析
我们先来看看崩溃时的调用堆栈信息
2020-09-25 07:13:53.630518+0800 类方法的归属[2941:85139] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[ZLStudent sayBad]: unrecognized selector sent to instance 0x1007b3da0'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff380b8b57 __exceptionPreprocess + 250
1 libobjc.A.dylib 0x00007fff70f2b5bf objc_exception_throw + 48
2 CoreFoundation 0x00007fff38137be7 -[NSObject(NSObject) __retain_OA] + 0
3 CoreFoundation 0x00007fff3801d3bb ___forwarding___ + 1427
4 CoreFoundation 0x00007fff3801cd98 _CF_forwarding_prep_0 + 120
6 libdyld.dylib 0x00007fff720d3cc9 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
可以看到,现在我们的首要目标就是找到 _CF_forwarding_prep_0
,我们可以看到,这个函数存在在 CoreFoundation
中,所以我们首先要把这个 Mach-O
拷出来,然后拖入到 Hopper Disassembler,我们在控制台使用 lldb
指令 image list
将系统库的路径都给输出出来,然后找到 CoreFoundation
库的路径
/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
拖入之后,我们开始查找 ___forwarding_prep_0___
下一步进入 ___forwarding_prep_0___
函数之后,找到 ____forwarding___
在 ____forwarding___
函数里面发现了forwardingTargetForSelector
如果没有实现 forwardingTargetForSelector
方法的话,则继续往下执行。我们阅读以下这里面的内容,其实做了一些判断,如果没有实现 methodSignatureForSelector
方法或者返回为nil的话,则最后会跳转到 loc_64e3c
我们继续往下看,这里也看到如果没有实现 methodSignatureForSelector
方法或者为空则跳转到 loc_64ec2
, 如果实现了 forwardInvocation
则会调用 forwardInvocation
这样,我们通过反汇编 CoreFoundation
的形势,也可以看到整个流程。
4、消息转发流程图
5、总结
如果一个实例方法不能在类和它的继承链的方法列表中不能被找到,则进入到方法解析和消息转发流程。
- 1 首先判断当前实例的类对象是否实现了
resolveInstanceMethod
方法,如果实现了,会调用resolveInstanceMethod
方法。这个时候我们可以在resolveInstanceMethod
方法里动态的添加该SEL
对应的方法。之后会 重新执行查找方法实现 的流程,如果依旧没找到方法实现,或者没有实现resolveInstanceMethod
方法,则进入消息转发流程。 - 2 调用
forwardingTargetForSelector
方法,尝试找到一个能响应该消息的对象。如果找到了,则直接把消息转发给它,如果返回nil,则继续下一步流程。 - 3 调用
methodSignatureForSelector
方法,尝试获得一个方法签名,如果获取不到,则直接调用doesNotRecognizeSelector
抛出异常信息。 - 4 调用
forwardInvocation
方法,进行事务处理。如果不处理的话,则把事务抛出,爱谁谁接。