注:在阅读本文之前,请先阅读我之前写的如下三篇文章:
- Runtime——消息转发流程
- Effective Objective-C 2.0——理解消息转发机制
- Runtime再理解
在上篇文章方法的查找流程——慢速查找中,在lookUpImpOrForward函数里面会进行方法的查找,如果最终没有找到,那么就会进入消息的转发流程,如下:
对象方法的动态方法决议
我们就从_class_resolveMethod函数开始研究,首先看其源码:
代码语言:javascript复制void _class_resolveMethod(Class cls, SEL sel, id inst)
{
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
如果所查找的方法是实例方法,那么就调用_class_resolveInstanceMethod;如果所查找的方法是类方法,那么就调用_class_resolveClassMethod。
我们先来看_class_resolveInstanceMethod:
代码语言:javascript复制static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, 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(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
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));
}
}
}
这里的主线就是调用OC中的 resolveInstanceMethod 方法。
上面代码中的第3~8行是一个容错判断,当找不到 resolveInstanceMethod 方法的实现的时候会走到这个判断里面来。但实际上,正常情况下所有继承自NSObject的类中,这个判断都是走不到的,因为 resolveInstanceMethod 方法是有默认实现的:
第10~11行的作用是,给cls发送一个名为SEL_resolveInstanceMethod(即resolveInstanceMethod)的消息,并且携带参数sel,这里携带的参数sel就是 resolveInstanceMethod 方法后面携带的参数。
需要注意的是,这里的cls是类对象;这里对 resolveInstanceMethod 方法的查找是一个缓存快速查找的过程,因为在前面的第3行代码处已经查找过一次了。
第13~16行,其作用是在cls中再次查找sel所对应的IMP。为啥需要再一次进行查找呢?不是没找到才走到这里的吗?原因就在于,我在第10~11行的时候,调用了OC中的 resolveInstanceMethod 方法,程序员可以在该方法中做相关操作。基于前面的分析,程序员需要在 resolveInstanceMethod 方法中做的操作是:给没有找到IMP的SEL动态添加一个IMP。
类方法的动态方法决议
通过上面_class_resolveMethod函数的源码我们知道,如果所查找的方法是实例方法,那么就调用_class_resolveInstanceMethod;如果所查找的方法是类方法,那么就调用_class_resolveClassMethod。
接下来我们就先来看看_class_resolveClassMethod的源码:
代码语言:javascript复制static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
assert(cls->isMetaClass());
if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(_class_getNonMetaClass(cls, inst),
SEL_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 = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
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 resolveClassMethod:%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));
}
}
}
第5~10行是容错处理。当找不到 resolveClassMethod 方法的实现的时候会走到这个判断里面来。但实际上,正常情况下所有继承自NSObject的类中,这个判断都是走不到的,因为 resolveClassMethod 方法是有默认实现的。
通过_class_resolveMethod函数的源码我们发现,当查找的是类方法的时候,在调用完函数之后,会再查找一遍对应的类方法,如果没有找到的话就会接着执行_class_resolveInstanceMethod函数。这是为什么呢?
当查找一个类方法没找到的时候,会进入类方法的动态方法决议,也就是调用_class_resolveClassMethod函数,此时我们如果在OC的 resolveClassMethod方法里面采取了对应操作并返回YES,那么消息转发到此结束;
如果我们在OC的 resolveClassMethod方法里面没有采取对应操作,那么程序会通过lookUpImpOrNil函数检测到还没有找到对应的方法实现,因此就会调用_class_resolveInstanceMethod函数进入实例方法的动态决议,测试的参数cls是元类,因此最终会在根元类的父类NSObject中去查找OC的 resolveInstanceMethod方法。
实例方法的动态决议只有一个 resolveInstanceMethod;类方法的动态决议,除了有一个 resolveClassMethod之外,还会走一遍实例方法动态决议 resolveInstanceMethod,其根本原因就是:要与【所有方法最终都要到NSObject的实例方法中去查找】这一点相呼应。
正因为当找不到对应的方法实现的时候,最终都会走到NSObject的 resolveInstanceMethod方法,因此我们就可以统一在NSObject的 resolveInstanceMethod 方法里面做处理。但是,我们不会这样去做,原因如下:
- 你如果要在NSObject里面做处理,那么势必会针对每个sel都做一次判断,这样的话,全工程中的判断都会集中在这里,一来代码太臃肿;二来耦合度很高,因为所有需要处理的地方都依赖这里。
- 如果其他人在对应的子类中也实现了resolveMethod方法,那么就不会走到NSObject的 resolveInstanceMethod 方法里面,那么这里面所做的处理就不会生效。基于此,在NSObject的 resolveInstanceMethod 方法里面做处理就很鸡肋。
消息的快速转发流程
如果在动态方法决议中没有做任何的处理,那么就会走到这里的快速消息转发流程。
代码语言:javascript复制- (id)forwardingTargetForSelector:(SEL)aSelector
在该方法里,系统给了将这个SEL转发给其他对象的机会。
其返回参数是一个对象,如果这个对象非nil、非self的话,系统会将运行的消息转发给这个对象执行。否则,会进入下面的消息慢速转发流程。
消息的慢速转发流程
当对象接收到某个消息之后,首先会去查找是否有该实现函数,如果有,那么就直接调用;如果没有,则进入消息转发流程。
首先会动态方法决议,给你自己或者你的父类提供一个内部特殊处理没有找到IMP的SEL的机会,如果你不处理,那么就进入消息快速转发流程。
消息快速转发,也就是将消息转发给别的对象,如果我不将消息转发给别的对象,那么就会进入到现在所讲的慢速消息转发流程。
慢速消息转发 的第一步就是你得告诉系统当前没有找到IMP的SEL的签名是什么,如下:
慢速消息转发 的第二步就是,你可以在下面的方法中对没有找到IMP的SEL进行相关的处理:
- (void)forwardInvocation:(NSInvocation *)anInvocation { //打印一下无法处理的方法名 NSLog(@"forwardInvocation, 无法处理的selector:%@", NSStringFromSelector(anInvocation.selector)); //如果SecondTextClass中能够响应该SEL,那么就让SecondTextClass中相应的SEL去执行该方法 SecondTextClass *secondTextObject = [[SecondTextClass alloc] init]; SEL sel = anInvocation.selector; if ([secondTextObject respondsToSelector:sel]) { [anInvocation invokeWithTarget:secondTextObject]; } //不再采取其他的操作的话,就代表不对找不到实现的SEL进行响应,这样程序就不会崩溃了。 }
可以将forwardInvocation函数理解成是一个秘书,它会随时准备响应老板的工作,有任务派给它它就会去处理任务,没有任务的话它就待命。这个函数还有一个作用就是它会拦截所有找不到IMP的SEL,要么处理它,要么让它失效,总之,不会让它走到崩溃的那一步。
需要注意的是,是否进行慢速消息转发流程的关键是,是否返回对应SEL的签名,只有返回了签名,才会进行慢速转发流程,否则会进入接下来的流程(即报崩溃)。
汇编分析
实际上,上面的分析是开了上帝视角的。如果不开上帝视角,那么该如何研究呢?其中一个角度就是汇编。
这里的Norman是一个自定义的类,里面声明了play方法,但是该方法并没有实现,此时运行,就会发生崩溃,崩溃信息如上图?。
然后我们分析左边的崩溃堆栈。4是当前定位到的位置,也就是[norman play];代码调用的位置,1是发送崩溃信息的位置,2【#2 0x000000010ac7e7bc in ___forwarding___ ()】就是消息转发的位置了,我们将光标点击到2,如下:
这里应该就是消息转发的流程了,然后我们往上翻,就会依次看到熟悉的字眼:
因此,我们就可以断定,这个汇编文件就是消息转发的研究对象,然后我就翻到最顶部,看看这个汇编文件叫啥名:
我们发现,它是CoreFoundation可执行文件中的___forwarding___。
所以我们就研究这里就可以了。
反汇编分析
但是毕竟我们很多人对汇编语言不是很熟悉,就单纯能去这么干巴巴看汇编代码的话,实际上很费劲,然后我就想,是不是有一些方法可以简化这里的研究,答案就是反汇编。
所谓的反汇编,实际上就是汇编的伪代码,这里给大家推荐两个反汇编的工具,一个叫【IDA Pro】,一个叫【Hopper Disassembler】
反汇编的对象就是可执行文件,接下来我们就去找CoreFoundation可执行文件。该可执行文件肯定是在系统包里面,因此,我们就需要到系统包里面去寻找。
在应用程序中找到Xcode,然后显示包内容:
然后按照如下路径找到iOS.simruntime:
然后显示iOS.simruntime的包内容,并按下面路径继续查找,最终找到CoreFoundation可执行文件:
然后我们就可以通过IDA或者Hopper来对该可执行文件进行反汇编分析了。
然后搜索【___forwarding___】就可以找到对应的反汇编伪代码了。
分析的时候可以对照汇编源码进行分析:
还是那句话,知识点不是最重要的,重要的是研究的方法和思路,通过反汇编进行底层探索是进阶高级必须掌握的技能。
以上。