前言
在上一篇方法的查找流程笔记的最后提出,如果我们调用的方法在缓存和method list中都没有找到的话,程序就会崩溃。针对这种情况,苹果给了三次自救的机会,消息转发机制三步曲:
第一步:动态方法决议
看源码
在方法的查找流程中分析lookUpImpOrForward源码的时候知道,在正常查找imp流程中找不到的话,会走到这个地方:
// No implementation found. Try method resolver once.if (resolver && !triedResolver) {runtimeLock.unlock();_class_resolveMethod(cls, sel, inst);runtimeLock.lock();// Don't cache the result; we don't hold the lock so it may have// changed already. Re-do the search from scratch instead.triedResolver = YES;goto retry;}
留意一下官方的注释 No implementation found. Try method resolver once. 受triedResolver控制,这个方法最多也就进去一次。直接看_class_resolveMethod源码:
/************************************************************************ _class_resolveMethod* Call +resolveClassMethod or +resolveInstanceMethod.* Returns nothing; any result would be potentially out-of-date already.* Does not check if the method already exists.**********************************************************************/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_resolveInstanceMethod* Call +resolveInstanceMethod, looking for a method to be added to class cls.* cls may be a metaclass or a non-meta class.* Does not check if the method already exists.**********************************************************************/static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst){// 在cls->ISA()(找元类及其父类)中SEL_resolveInstanceMethod的实现// 就是苹果自己的方法不能崩溃啊,集成自NSObject的都会有// 在NSObject.mm 文件中有实现,// 这样保证,你没有实现,通过循环向上查找会找到实现,不至于SEL_resolveInstanceMethod造成找不到imp而崩溃// + (BOOL)resolveInstanceMethod:(SEL)sel {// return NO;// }if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,NO/*initialize*/, YES/*cache*/, NO/*resolver*/)){// Resolver not implemented.return;}// 找到之后向SEL_resolveInstanceMethod发个消息,调用一下// 这也就是当我们没有实现方法的时候,苹果回调给的一次自救机会BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);// 后边的先不用看了,就是查验一下,动态决议我们有没有做处理(添加imp)。// Cache the result (good or bad) so the resolver doesn't fire next time.// +resolveInstanceMethod adds to self a.k.a. clsIMP 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));}}}
上边已经通过备注的方式分析了源码的逻辑。至于_class_resolveClassMethod实现的逻辑和上边的大同小异,原理是一致的。
动态决议到底要做什么
通过看源码了解到在正常查找imp的时候找不到,系统会走动态决议这个回调。但是回调中我们要做什么呢?
想一下,方法调用说白了,就是通过sel寻找imp,找到方法的实现的过程。现在的问题是找不到imp了,那么怎么办?我们给这个sel添加一个已有的imp不就行了吗。就相当于,目录sel为标题,imp为页码,我们给出一个书中有的页码,这样通过sel就能找到你想要的内容,就行了呗。
所以,我们要做的,就是,给sel,添加一个imp,再指定一下types(符号签名),构成一个新的method,添加到类(或者元类,对象方法存在类中,类方法存在元类中)。method的结构是这样,我们自己构造:
struct method_t {SEL name;const char *types;MethodListIMP imp;};
逻辑知道了,代码怎么写啊?很不幸,runtime提供了相应的api,下一步,代码演示。
代码演示
实例方法的动态决议会回调到:+(BOOL)resolveInstanceMethod:(SEL)sel; 类方法的动态决议会回调到:+(BOOL)resolveClassMethod:(SEL)sel;
在Student类中没有定义saySomething方法和sayClassOver方法,这里分别通过对象和类两种方式调用:
在Student分别重写两个决议方法,来处理:
对saySomething的处理是,获取到实例方法studentA的imp和types,然后通过class_addMethod向类中添加,这样,在调用saySomething时查找到的方法实现,实际上就是studentA的方法实现;
对sayClassOver的处理是,获取到类方法studentB的imp和types,然后通过class_addMethod向元类中添加,这样,在调用sayClassOver时查找到的方法实现,实际上就是studentB的方法实现;
控制台打印输出,程序没有崩溃,自救成功。
思考1
在上边动态决议的时候,我们都常规的将实例方法saySomething添加studentA实例方法的imp,将类方法sayClassOver添加studentB类方法的imp。那么能不能交换?saySomething添加studentB的imp,sayClassOver添加studentA的imp呢???
解答:可以。
上边提到过,动态方法决议,就是给sel对应一个imp,然后加上types构成新的method,加到对应的类或者元类中,至于这个imp是在元类还是类中,都可,你只要能让我找到实现就行。但是要注意一点,如果是实例方法的动态决议,class_addMethod的第一个参数必须是类;如果是类方法的动态决议,class_addMethod的第一个参数必须是元类。(因为实例方法在类中,类方法存在元类中,添加完成之后,还会走正常查找流程去找,如果添加的不准确,还是找不到,同样会报错)。
代码验证一下:
思考2
在_class_resolveMethod中有一个逻辑判断,如果是非元类就直接走_class_resolveInstanceMethod,如果是元类的话,会尝试_class_resolveClassMethod并且在做lookUpImpOrNil查找之后如果没有找到imp,会再走一遍_class_resolveInstanceMethod,为什么?
解答:根元类的父类是NSObject类,没问题吧(isa和继承关系走位图很明显)?那么[Student nsobjectA];通过Student类可以直接调用NSObject分类中定义的nsobjectA实例方法。方法的查找流程上解释过。
当类方法查找_class_resolveClassMethod动态决议未果之后,要再走一次_class_resolveInstanceMethod,就是因为你通过Student类调用的方法(你以为的类方法),在NSObject中可能作为实例方法存在,那么同样,NSObject也可以在+(BOOL)resolveInstanceMethod:(SEL)sel;中对它进行决议。解释的一点毛病没有。
再有一个问题,为什么这次走的_class_resolveInstanceMethod会直接找到NSObject类中的+(BOOL)resolveInstanceMethod:(SEL)sel;而不是Student中的+(BOOL)resolveInstanceMethod:(SEL)sel;?
(这个是我的探索猜测,不一定准确,有新的认识再更改)
这个需要看上边_class_resolveMethod源码:
在调用_class_resolveInstanceMethod时的第一个参数是Student元类,没问题吧?传进来看上边_class_resolveInstanceMethod源码中:
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
向cls(Student元类)中调用resolveInstanceMethod,元类我们开发人员也没法编辑和控制啊?所以就沿着继承关系图,最终走到了NSObject类中,响应了NSObject类中的+(BOOL)resolveInstanceMethod:(SEL)sel;
Tips:在_class_resolveClassMethod源码中:通过_class_getNonMetaClass获取cls(元类)的普通类,来调用普通类中实现的resolveClassMethod。进一步验证了上边提到的会走到NSObject的判断。
代码验证一下上边的判断,会走到NSObject类中的+(BOOL)resolveInstanceMethod:(SEL)sel; 而不是Student类中的+(BOOL)resolveInstanceMethod:(SEL)sel;
通过 [Student performSelector:@selector(sayClassOver)]; 调用:
在Student类中:并没有回调到resolveInstanceMethod,程序崩溃;
在NSObject分类中实现:和探索推断一致,走到了resolveInstanceMethod中成功添加了imp,程序没有崩溃。
动态方法决议之后,goto retry,再重新尝试一下方法流程查找,如果在动态决议的时候,我们手动处理了添加了method,成功找到的话,好,程序没问题,如果再找不到,那么就开始进入新的流程,真正意义的消息转发。
第二步:快速转发
动态决议依旧找不到imp,程序开始进行消息转发流程。直白来说,就是,动态方法决议就是在自己的类及父类直至NSObject中找+(BOOL)resolveInstanceMethod:(SEL)sel和+(BOOL)resolveClassMethod:(SEL)sel的重写,开发者如果都没有处理,那么系统就认为就是你这个类及父类处理不了这个消息,那么好,交给别的类来处理。比如:Student中及父类Person直至NSObject无法处理saySomething这个方法,而恰好Teacher(Teacher类继承自NSObject,和Student类没关系)这个类中有saySomething这个方法,好,那么交给他处理好了。
快速转发API
同样,快速转发也分为实例方法和类方法的回调,分别是(为什么是这两个方法,在源码中没有直接调用,通过查看方法调用日志来探索找到的,这里就不分析了):
- (id)forwardingTargetForSelector:(SEL)aSelector; 用来处理实例方法,返回可以处理实例方法的对象。
+ (id)forwardingTargetForSelector:(SEL)sel; 用来处理类方法,返回可以处理类方法的类对象。
快速转发代码实现
Teacher类中实现了Student及其父类中尚未实现的对象方法和类方法:
在main方法中调用:
目前的场景是,Student类及其父类没有实现这两个方法,系统找不到imp开始走动态方法决议,决议依旧没有实现,接下来开始快速转发,在Student中分别实现- (id)forwardingTargetForSelector:(SEL)aSelector; 和
+ (id)forwardingTargetForSelector:(SEL)sel:
结果:
Student发送的消息,已经被Teacher处理掉了,消息转发成功。大吉大利。
第三步:慢速转发
如果动态方法决议没有处理,快速转发,也没有返回能够处理掉消息的对象。那么接下来,就走到了慢速转发这一步,为什么称之为慢速转发?说白点,就是,这个消息我处理不掉,我也不知道谁能处理掉,我就仍在这儿了,谁能处理谁处理吧。
慢速转发API
(查看调用日志找到的调用顺序,这里作总结)慢速转发开始,先要回调methodSignatureForSelector方法获取到方法签名,然后将将消息封装成NSInvocation对象,回调forwardInvocation。
实例方法的慢速转发回调:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
类方法的慢速转发回调:
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
+ (void)forwardInvocation:(NSInvocation *)anInvocation;
在说这两个API的使用之前,先看看为什么找不到imp崩溃的程序,系统是怎么写的。现在已经了解到,动态方法决议和快速转发都没有处理掉消息之后,会回调获取方法签名,最终的最终会回调forwardInvocation方法,如果这个我们依旧没有重写,就会崩溃,为什么?看NSObject.mm源码:
+ (void)forwardInvocation:(NSInvocation *)invocation {[self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];}- (void)forwardInvocation:(NSInvocation *)invocation {[self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];}// Replaced by CF (throws an NSException)+ (void)doesNotRecognizeSelector:(SEL)sel {_objc_fatal("+[%s %s]: unrecognized selector sent to instance %p",class_getName(self), sel_getName(sel), self);}// 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);}
NSObject中实现了这个,在forwardInvocation中调用doesNotRecognizeSelector,_objc_fatal就使程序崩溃了,并爆出了找不到方法的错误。这就是最终崩溃的点。
慢速转发代码实现
同上在main中通过Student调用两个未实现的方法,然后在Student中实现这两个慢速转发的API(之前动态方法决议和快速转发的代码都注释掉,场景就是之前的处理没有):
方法签名的相关参照方法签名简介。看到代码发现什么没?我们真正处理掉消息了吗?没有啊!程序也没有崩溃,就好似,消息被抛弃了一样,谁也处理不掉,就神游去吧。我们可以在forwardInvocation中做一些if-else判断,谁能处理谁就处理吧:
总结
动态方法决议和消息转发的相关知识点就先写到这里,等有了新的了解随时补充。
