在 Objective-C 语言中,对象/类(其实类也是一个对象) 执行方法最后会转化成给对象发送消息:
objc_msgSend(receiver, @selector(message))
如果 reveiver
中没有找到对应方法 message
, 则会开始消息转发的过程,也就是过程:
- 动态方法解析 Method Resolution
- 快速转发 Fast Rorwarding
- 完整消息转发 Normal Forwarding
接下来通过OC的源码来分析以上几个步骤具体的调用过程
消息发送
当在 OC 中给对象执行方法,如 [object foo]
,会被翻译为 objc_msgSend(object, @selector(foo))
,@seletor
会将 foo 方法生成对应的选择子(SEL),选择子只跟方法名有关系,不同的类之间可以存在相同的方法选择子,但是同一个类(及类的继承体系)中,不能存在2个同名的方法,即使参数类型不同也不行,也就是说 OC 中不支持像 C++ 那样的函数重载。
当编译器遇到一个方法调用时,它会将方法的调用翻译成以下函数中的一个
objc_msgSend
、objc_msgSend_stret
、objc_msgSendSuper
和objc_msgSendSuper_stret
。 发送给对象的父类的消息会使用objc_msgSendSuper
有数据结构作为返回值的方法会使用objc_msgSendSuper_stret
或objc_msgSend_stret
其它的消息都是使用objc_msgSend
发送的
objc_msgSend
的具体实现是由汇编语言编写的,其中具体过程细节可以参考我另一篇文章objc-msg-arm64源码深入分析
objc_msgSend
函数执行过程中,如果根据 SEL 在接受者(object)方法列表的 cache 缓存中没有查找到对应的方法 IMP,会执行 C 语言函数 __class_lookupMethodAndLoadCache3
1 | IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls) |
这个方法只允许被 _objc_msgSend
内部调度,其他方式应该使用 lookUpImp
此函数将忽略缓存查询,因为执行此函数之前能确保已经查询过对应的内存
1 | IMP lookUpImpOrForward(Class cls, SEL sel, id inst, |
整个方法查找的过程,可以简单的概括为以下几个步骤
- 实现、初始化对应的类
- 根据是否支持垃圾回收机制(GC)判断是否忽略当前的方法调用
- 从cache中查找方法
- cache中没有找到对应的方法,则到方法列表中查,查到则缓存
- 如果本类中查询到没有结果,则遍历所有父类重复上面的查找过程
- 最后都没有找到的方法的话,则执行
_class_resolveMethod
让调用者动态添加方法,并重复一轮查询方法的过程 - 若第六步没有完成动态添加方法,则把 _objc_msgForward_impcache 作为对应 SEL 的方法进行缓存,然后调用 _objc_msgForward_impcache 方法
动态方法解析
消息发送的过程中,如果没有找到先进行 _class_resolveMethod
允许开发者动态的根据 SEL 实现对应的 IMP,实现前先执行 runtimeLock.unlockRead()
打开了读锁,所以开发者在此动态实现的过程添加了方法实现,故不需要缓存方法;
_class_resolveMethod
调用过程又是非原子性的,执行完的时候方法列表可能已经更新了,所以执行完了之后需要重复一轮查询方法的过程
1 | void _class_resolveMethod(Class cls, SEL sel, id inst) |
如果 cls 不是元类,则执行 _class_resolveInstanceMethod
函数;否则 cls 属于元类则会调用 _class_resolveClassMethod
,然后执行 lookUpImpOrNil
1 | IMP lookUpImpOrNil(Class cls, SEL sel, id inst, |
lookUpImpOrNil
和 lookUpImpOrForward
类似,前者内部是先调用后者函数,判断返回 imp 结果是否和 _objc_msgForward_impcache 相同,如果相同返回 nil,反之返回 imp。
需要注意的是在 lookUpImpOrNil
中并不会对 cls 进行初始化(initialize)或者是方法动态实现过程(resolver),若 lookUpImpOrNil
返回了nil,则会调用 _class_resolveInstanceMethod
这里以非元类来分析
1 | static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst) |
到此,消息转发前的逻辑已经全部走完,简单总结一下各个函数调用的顺序作用:
- 汇编入口
_objc_msgSend
为消息发送的入口 - 找不到方法则跳转到
__objc_msgSend_uncached_impcache
,对栈进行相关操作 - 跳转
_class_lookupMethodAndLoadCache3
(objc-runtime-new.mm) - 第一次执行
lookUpImpOrForward
, 对相关类进行 initialize 相关操作,忽略缓存列表去查找方法,如果找不到会进行 reslover 动态方法解析 - 步骤4会一直从本类到父类进行重复查找,如果都没有找到方法则调用
_class_resolveMethod
进行方法动态解析 - 如果是非元类,则直接跳转到
_class_resolveInstanceMethod
,函数内部会先调用lookUpImpOrNil
来判断类有没有实现+resolveInstanceMethod
方法,这里的查找结果也会缓存到 cache 中,内部查找也是通过lookUpImpOrForward
来实现,根据返回的imp是否为_objc_msgForward_impcache
,若是则返回 nil,然后_class_resolveClassMethod
会直接return,结束动态解析过程 - 若
+resolveClassMethod
被实现,则同过objc_msgSend
来执行+resolveClassMethod
方法;缓存结果,减少_class_resolveClassMethod
过程调用
消息转发
在第一次执行 lookUpImpOrForward
过程中,动态解析方法完了之后,还没有找到方法,则放回 _objc_msgForward_impcache
__objc_msgSend_uncached_impcache
汇编代码会利用 br 指令跳转到 _objc_msgForward_impcache
,后者内部是通过 b 指令跳转到 __objc_msgForward
,最后会调用 _objc_forward_handler
函数(objc-runtime.h)
_objc_msgSend_uncached_impcache
的默认实现为 objc_defaultForwardHandler
1 | __attribute__((noreturn)) void |
从代码实现中可以看到熟悉的报错日志:unrecognized selector sent to instance
要自定义转发过程则需要通过 objc_setForwardHandler
来重写 objc_defaultForwardHandler
1 | void objc_setForwardHandler(void *fwd, void *fwd_stret) |
objc_setForwardHandler
的调用是在 Core Foundation 中实现,但在其开源代码中,苹果删除了件 __CFInitialize()
中调用 objc_setForwardHandler
的代码。具体可以参考文章Objective-C 消息发送与转发机制原理
消息转发的过程大概可以分为以下几步:
- 快速转发:开发者通过重写
forwardingTargetForSelector:
方法提供新的接受者(forwardingTarget)来重新执行 seletor;如果 forwardingTarget 和旧的接受者相同或者为nil,则进入下一步 - 完整消息转发:重写
methodSignatureForSelector:
方法获取方法签名并新建一个 NSInvocation 对象 invocation,invocation作为参数传入开发者重写的forwardInvocation:
方法从而完成整个消息的转发 - 若步骤2没有完成转发则会调用
doesNotRecognizeSelector
方法,抛出异常
消息转发特性能做什么?
了解过消息转发的过程,那我们能利用这特性解决什么问题呢?
1. AOP
既然能接管消息转发的过程,很容易联想到通过消息转发在原有方法执行的过程中插入需要的代码逻辑,从而实现切面编程,具体可以参考forwardInvocation的例子
2.解决NSTimer强引用Target导致循环引用
跟第一个例子相似,也是通过 NSProxy 进行消息转发,在原有 NSTimer 和target 之间加入一层proxy解决循环引用问题
1 | - (id)forwardingTargetForSelector:(SEL)aSelector { |
参考文章: