开发中我们可能遇到过这样的异常:unrecognized selector sent to xxxxxx
,当我们调用的方法没有对应的实现(IMP
)时,系统会抛出如上异常。但是在抛出异常之前,我们还有三次补救的机会:
- 动态方法解析:Dynamic Method Resolution
- 快速转发:Fast Forwarding
- 正常转发:Normal Forwarding
这个就是Objective-C的消息转发。从上到下,进行消息转发的开销越来越大。网上有关这三个步骤的介绍很多,这里就不再重复了。本文着重从源码角度分析一下为什么 Message Forward 是由这几个步骤组成,以及苹果设计的 Message Forward 机制有什么具体作用等。
1、objc_msgSend
我们知道,Objective-C的方法调用实际上会转换成消息发送,比如:
1 | [obj message]; |
最终会转换成objc_msgSend
函数调用:objc_msgSend(receiver, @selector(message));
。有关objc_msgSend
函数,它的定义是id objc_msgSend(id self, SEL _cmd, ...)
,第一个参数为接收消息的对象,第二个参数为要调用的函数实现的选择子SEL
,这两个参数为隐式参数,由系统自动生成,...
为可变参数列表。我在之前的一篇文章中从汇编的角度分析过其实现原理,objc_msgSend
函数由汇编实现,它的作用是:
- 根据传入的对象,获取其isa指针,并根据isa指针找到对象所属的类
- 获取类的方法缓存,并根据selector从缓存中查找对应的IMP
- 如果从缓存中找到IMP,直接调用
- 如果没有从缓存中找到,调用C函数(__class_lookupMethodAndLoadCache3)继续查询,并将查询结果存入类的方法缓存
这一小节分析一下ARM64平台下objc_msgSend
函数的实现,由于汇编代码看起来太不直观,这里就不贴出来了,将其转换成C伪代码(为了简化流程,省略TaggedPointer
部分),有兴趣的同学可以看下objc-msg-arm64.s
文件中的汇编源码。
1 | id objc_msgSend(id self, SEL _cmd, ...) { |
至此,objc_msgSend
函数中的IMP
查找流程比较清晰了:
- 首先从缓存中查找,如果当前类没有找到,继续向其父类查找,直到查询到
NSObject
为止 - 如果步骤1没有找到对应的
IMP
,在类的未缓存的方法列表中查找,根据isa指针,直到查找到NSObject
为止 - 如果步骤2也没有找到,转到
_class_lookupMethodAndLoadCache3
,调用lookUpImpOrForward
继续查找并进入消息转发流程。
2、lookUpImpOrForward与Message Forwarding
先看下lookUpImpOrForward
函数的源码:
1 | IMP lookUpImpOrForward(Class cls, SEL sel, id inst, |
lookUpImpOrForward
会从当前类及其父类的的方法缓存和方法列表中查找IMP
,如果未找到,执行动态方法解析,如果动态方法解析也失败,进入Message Forwarding
流程。
2.1 动态方法解析
_class_resolveMethod
调用_class_resolveInstanceMethod
查看当前类是否实现了resolveInstanceMethod
方法(对于类方法,对应的是resolveClassMethod
),如果实现了,resolved
置true
,重新执行一次lookUpImpOrNil
流程,将新的IMP
加入缓存并执行。
1 | static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst) |
2.2 消息转发
上面讲到,当方法缓存、方法列表和动态方法解析都未能找到IMP
时,会调用_objc_msgForward_impcache
进入forwarding
流程,但是关于这个函数,我只在源码中找到了如下实现:
1 | /******************************************************************** |
可以看到这里涉及到了__objc_forward_handler
的设置和读取。
1 | 2 CoreFoundation 0x00000001086a1f44 -[NSObject(NSObject) doesNotRecognizeSelector:] + 132 |
从unrecognized selector sent to xxx
崩溃是的堆栈信息可以看到,这里还涉及到了CF框架中的_CF_forwarding_prep_0
和___forwarding___
两个函数,有关转发的所有逻辑都在___forwarding___
中,而苹果公布的CoreFoundation
源码中删去了这部分内容。网上大神对__CFInitialize
进行逆向后得到了___forwarding___
的伪代码:
1 | int __forwarding__(void *frameStackPointer, int isStret) { |
___forwarding___
包含了整个消息转发路径的逻辑,概括如下:
- 先调用
forwardingTargetForSelector
方法获取新的target
作为receiver
重新执行selector
,如果返回的内容不合法(为nil
或者跟旧receiver
一样),那就进入第二步。 - 调用
methodSignatureForSelector
获取方法签名后,判断返回类型信息是否正确,再调用forwardingTargetForSelector
执行NSInvocation
对象,并将结果返回。如果对象没实现methodSignatureForSelector
方法,进入第三步。 - 调用
doesNotRecognizeSelector
方法。
2.3 小结
借用网上的一张图片对Objective-C的消息发送和转发流程总结一下:
3、消息转发机制的应用
说了这么多,了解消息转发的过程对开发有什么帮助呢,或者消息转发有什么实际的应用呢?
3.1 weak proxy
在使用NSTimer或者CADisplayLink等定时器时,Timer会对其target(一般是self
)进行强引用,稍微不注意可能会出现内存泄露问题,这时可以创建一个WeakProxy
对象,由该对象弱引用self
,将Timer的target设置为WeakProxy
对象,并使用消息转发机制,将所有发送给WeakProxy
的消息转发给self
。由此打破retain cycle链,解决内存泄露问题。YYKit
框架中就使用了这种方法(YYWeakProxy
)。
1 | @interface WeakProxy: NSProxy |
3.2 多重代理
代理一般是一对一的,如果开发中遇到这样的问题:对某处做了修改,希望同时在多个页面更新。比如在聊天时,给对方加个备注,需要在资料页面、聊天详情页面、联系人列表页同步更新。这时可以用多重代理的方法实现。而多重代理的一种实现方式就是用消息转发。
设计一个中间层,中间层接收消息并负责转发给不同的页面。
具体代码可以参考别人写的一个Demo
3.3 模拟多继承
来自Wikipedia:
面向对象的程序设计中,继承描述了两种类型或两个类的对象,其中一种是另外一种的“子类型”或“子类”。子类继承了父类的特征,允许分享功能。例如,可以创造一个“哺乳类动物”类别,拥有进食、繁殖等的功能;然后定义一个子类型“猫”,它可以从父类继承上述功能,不需重新编写程序,同时增加属于自己的新功能,例如“追赶老鼠”。
然而,如果想同时自多于一个结构继承,例如容许“猫”继承“哺乳类动物”之余,同时继承“卡通角色”和“宠物”,缺乏多重继承往往会导致十分笨拙的混合继承,或迫使同一个功能在多于一个地方被重写。(这带来了维护上的问题)
虽然多重继承可以导致某些令人混淆的情况,但是在OOP中它还是有优点的。但是OC并没有实现真正意义上的多继承 (Swift与Java类似,同样不允许一个类继承自多个类,只能通过遵循多个协议来间接实现多继承),对于多继承,我们可以借助消息转发来实现。
示例代码如下:
1 | // Father类,有一个方法:work |
在Son
类中重载了- (id)forwardingTargetForSelector:(SEL)aSelector;
方法,当它无法处理消息时,系统会调用这个方法将消息转发给指定的target。测试代码和结果如下:
1 | Son *son = [[Son alloc] init]; |
我们还可以利用forwardInvocation
进行消息转发,对上述的Son
类进行改造,也能达到同样的类似多继承的效果。
1 | @implementation Son |