前言

在上一篇方法的查找流程笔记的最后提出,如果我们调用的方法在缓存和method list中都没有找到的话,程序就会崩溃。针对这种情况,苹果给了三次自救的机会,消息转发机制三步曲:
消息转发三部曲.jpg

第一步:动态方法决议

看源码

方法的查找流程中分析lookUpImpOrForward源码的时候知道,在正常查找imp流程中找不到的话,会走到这个地方:

  1. // No implementation found. Try method resolver once.
  2. if (resolver && !triedResolver) {
  3. runtimeLock.unlock();
  4. _class_resolveMethod(cls, sel, inst);
  5. runtimeLock.lock();
  6. // Don't cache the result; we don't hold the lock so it may have
  7. // changed already. Re-do the search from scratch instead.
  8. triedResolver = YES;
  9. goto retry;
  10. }

留意一下官方的注释 No implementation found. Try method resolver once. triedResolver控制,这个方法最多也就进去一次。直接看_class_resolveMethod源码:

  1. /***********************************************************************
  2. * _class_resolveMethod
  3. * Call +resolveClassMethod or +resolveInstanceMethod.
  4. * Returns nothing; any result would be potentially out-of-date already.
  5. * Does not check if the method already exists.
  6. **********************************************************************/
  7. void _class_resolveMethod(Class cls, SEL sel, id inst)
  8. {
  9. if (! cls->isMetaClass()) {
  10. // try [cls resolveInstanceMethod:sel]
  11. _class_resolveInstanceMethod(cls, sel, inst);
  12. }
  13. else {
  14. // try [nonMetaClass resolveClassMethod:sel]
  15. // and [cls resolveInstanceMethod:sel]
  16. _class_resolveClassMethod(cls, sel, inst);
  17. if (!lookUpImpOrNil(cls, sel, inst,
  18. NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
  19. {
  20. _class_resolveInstanceMethod(cls, sel, inst);
  21. }
  22. }
  23. }

直接先跟进_class_resolveInstanceMethod源码:

  1. /***********************************************************************
  2. * _class_resolveInstanceMethod
  3. * Call +resolveInstanceMethod, looking for a method to be added to class cls.
  4. * cls may be a metaclass or a non-meta class.
  5. * Does not check if the method already exists.
  6. **********************************************************************/
  7. static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
  8. {
  9. // 在cls->ISA()(找元类及其父类)中SEL_resolveInstanceMethod的实现
  10. // 就是苹果自己的方法不能崩溃啊,集成自NSObject的都会有
  11. // 在NSObject.mm 文件中有实现,
  12. // 这样保证,你没有实现,通过循环向上查找会找到实现,不至于SEL_resolveInstanceMethod造成找不到imp而崩溃
  13. // + (BOOL)resolveInstanceMethod:(SEL)sel {
  14. // return NO;
  15. // }
  16. if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
  17. NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
  18. {
  19. // Resolver not implemented.
  20. return;
  21. }
  22. // 找到之后向SEL_resolveInstanceMethod发个消息,调用一下
  23. // 这也就是当我们没有实现方法的时候,苹果回调给的一次自救机会
  24. BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
  25. bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
  26. // 后边的先不用看了,就是查验一下,动态决议我们有没有做处理(添加imp)。
  27. // Cache the result (good or bad) so the resolver doesn't fire next time.
  28. // +resolveInstanceMethod adds to self a.k.a. cls
  29. IMP imp = lookUpImpOrNil(cls, sel, inst,
  30. NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
  31. if (resolved && PrintResolving) {
  32. if (imp) {
  33. _objc_inform("RESOLVE: method %c[%s %s] "
  34. "dynamically resolved to %p",
  35. cls->isMetaClass() ? '+' : '-',
  36. cls->nameForLogging(), sel_getName(sel), imp);
  37. }
  38. else {
  39. // Method resolver didn't add anything?
  40. _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
  41. ", but no new implementation of %c[%s %s] was found",
  42. cls->nameForLogging(), sel_getName(sel),
  43. cls->isMetaClass() ? '+' : '-',
  44. cls->nameForLogging(), sel_getName(sel));
  45. }
  46. }
  47. }

上边已经通过备注的方式分析了源码的逻辑。至于_class_resolveClassMethod实现的逻辑和上边的大同小异,原理是一致的。

动态决议到底要做什么

通过看源码了解到在正常查找imp的时候找不到,系统会走动态决议这个回调。但是回调中我们要做什么呢?
想一下,方法调用说白了,就是通过sel寻找imp,找到方法的实现的过程。现在的问题是找不到imp了,那么怎么办?我们给这个sel添加一个已有的imp不就行了吗。就相当于,目录sel为标题,imp为页码,我们给出一个书中有的页码,这样通过sel就能找到你想要的内容,就行了呗。
所以,我们要做的,就是,给sel,添加一个imp,再指定一下types(符号签名),构成一个新的method,添加到类(或者元类,对象方法存在类中,类方法存在元类中)。method的结构是这样,我们自己构造:

  1. struct method_t {
  2. SEL name;
  3. const char *types;
  4. MethodListIMP imp;
  5. };

逻辑知道了,代码怎么写啊?很不幸,runtime提供了相应的api,下一步,代码演示。

代码演示

实例方法的动态决议会回调到:+(BOOL)resolveInstanceMethod:(SEL)sel; 类方法的动态决议会回调到:+(BOOL)resolveClassMethod:(SEL)sel;

Student类中没有定义saySomething方法和sayClassOver方法,这里分别通过对象和类两种方式调用:
截屏2020-04-17 14.07.46.png
在Student分别重写两个决议方法,来处理:
截屏2020-04-17 14.08.30.png
saySomething的处理是,获取到实例方法studentAimptypes,然后通过class_addMethod向类中添加,这样,在调用saySomething时查找到的方法实现,实际上就是studentA的方法实现;
sayClassOver的处理是,获取到类方法studentBimptypes,然后通过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的第一个参数必须是元类。(因为实例方法在类中,类方法存在元类中,添加完成之后,还会走正常查找流程去找,如果添加的不准确,还是找不到,同样会报错)。
代码验证一下:
截屏2020-04-17 14.18.25.png

思考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源码中:

  1. BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
  2. 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)]; 调用:
截屏2020-04-17 18.55.12.png
在Student类中:并没有回调到resolveInstanceMethod,程序崩溃;
截屏2020-04-17 18.57.07.png
在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及其父类中尚未实现的对象方法和类方法:
截屏2020-04-19 15.28.32.png
在main方法中调用:
截屏2020-04-19 15.30.52.png
目前的场景是,Student类及其父类没有实现这两个方法,系统找不到imp开始走动态方法决议,决议依旧没有实现,接下来开始快速转发,在Student中分别实现- (id)forwardingTargetForSelector:(SEL)aSelector;
+ (id)forwardingTargetForSelector:(SEL)sel:
截屏2020-04-19 15.26.00.png
结果:
截屏2020-04-19 15.26.32.png
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源码:

  1. + (void)forwardInvocation:(NSInvocation *)invocation {
  2. [self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
  3. }
  4. - (void)forwardInvocation:(NSInvocation *)invocation {
  5. [self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
  6. }
  7. // Replaced by CF (throws an NSException)
  8. + (void)doesNotRecognizeSelector:(SEL)sel {
  9. _objc_fatal("+[%s %s]: unrecognized selector sent to instance %p",
  10. class_getName(self), sel_getName(sel), self);
  11. }
  12. // Replaced by CF (throws an NSException)
  13. - (void)doesNotRecognizeSelector:(SEL)sel {
  14. _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p",
  15. object_getClassName(self), sel_getName(sel), self);
  16. }

NSObject中实现了这个,在forwardInvocation中调用doesNotRecognizeSelector,_objc_fatal就使程序崩溃了,并爆出了找不到方法的错误。这就是最终崩溃的点。

慢速转发代码实现

同上在main中通过Student调用两个未实现的方法,然后在Student中实现这两个慢速转发的API(之前动态方法决议和快速转发的代码都注释掉,场景就是之前的处理没有):
截屏2020-04-19 16.01.43.png
方法签名的相关参照方法签名简介。看到代码发现什么没?我们真正处理掉消息了吗?没有啊!程序也没有崩溃,就好似,消息被抛弃了一样,谁也处理不掉,就神游去吧。我们可以在forwardInvocation中做一些if-else判断,谁能处理谁就处理吧:
截屏2020-04-19 16.10.34.png

总结

动态方法决议和消息转发的相关知识点就先写到这里,等有了新的了解随时补充。