runtime(四)objc-msgSend
2020-7-10
| 2024-5-16
0  |  阅读时长 0 分钟
type
status
date
slug
summary
tags
category
icon
password

一、介绍与应用

1.1 objc_msgSend

Objective-C中调用方法,称为消息传递,消息有名称(name)选择子(selector),可以接收参数,而且可能还有返回值。
objc_msgSend其实就是消息传递在底层C语言的函数实现,在Objective-C中,大部分方法调用都是经过objc_msgSend来实现的。当然,除去load方法等特殊情况。
一般,给对象发送消息如下:
someObject是消息接收者,messageName是选择子选择子参数合起来称为消息。在底层,编译器收到之后,将其转换obj_msgSend函数,其函数声明如下:
  • 第一个参数为消息接收者
  • 第二个参数为SEL
上面给对象发送之后转换即为:
objc_msgSend会根据接受者与选择子的类型来调用适当的方法。

1.2 应用

下面我们通过直接调用objc_msgSend方法,来看看它是如何调用的。
将上面方法,改为objc_msgSend调用如下:
上面代码,在这儿

1.3 为什么需要objc_msgSend

在C语言中,使用静态绑定(static binding)来实现函数调用,即在编译期就决定运行时所应调用的函数。
看一个实例:
如果不考虑内联,那么编译器在编译代码的时候就已经知道程序中有printHelloprintGoodbye的函数了,于是就直接生成调用这些函数的指令。而函数地址实际上是硬编码在指令之中。
若是将刚才的代码改成下面:
这时就用到了动态绑定,因为所要调用的函数直到运行期才能确定。编译器在这种情况下生成的指令与刚才不同,在第一个例子中,ifelse都有函数调用指令。而在第二个例子中,只有一个函数调用指令,不过待调用的函数地址无法硬编码在指令之中,而是要在运行期读取出来。
Objective-C,如果要向某对象发送消息,就会使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的C语言函数,然而对象收到消息之后,调用哪个方法则由运行期决定,甚至可以在运行期改变。
objc_msgSend,就是承载Objective-C动态绑定机制的函数。

1.4 更多的objc_msgSend函数

类比objc_msgSend函数,还有几个类似的方法可以在<objc/message.h>头文件里找到:
关于上面三个函数,摘抄一段说明:
objc_msgSend_stret:如果待发送的消息要返回结构体,那么可交由此函数处理。只有当CPU的寄存器能够容纳得下消息返回类型时,这个函数才能处理此消息。若是返回值无法容纳于CPU寄存器中(比如说返回的结构体太大了),那么就由另一个函数执行派发。此时,那个函数会通过分配在栈上的某个变量来处理消息所返回的结构体。
objc_msgSend_fpret:如果消息返回的是浮点数,那么可交由此函数处理。在某些架构的CPU中调用函数时,需要对“浮点数寄存器”(floating-point register)做特殊处理,在现代架构(如x86-64和ARM)上,浮点数和整数都可以通过通用寄存器返回,因此objc_msgSend_fpret在这些平台上可能不再必要,直接使用objc_msgSend即可处理浮点数返回值。
objc_msgSendSuper:用于向父类(superclass)发送消息,而不是向当前对象(self)发送。它接受一个objc_super结构体作为第一个参数,该结构体包含两个成员:receiver(实际的对象实例)和super_class(指向父类的类对象指针)。这个函数允许在子类中通过super关键字调用父类的方法实现,支持多态和方法覆盖的正确行为。例如,在子类的实现中调用[super someMethod]时,就是通过objc_msgSendSuper在幕后实现的。

二、探索objc_msgSend

2.1 分阶段流程

notion image

2.2 源码导读

透过汇编的流程,对流程进行梳理:
notion image
接下来的进入源码:
notion image
其中,最后一步:_objc_msgForward_impcache又是汇编,但是有高手已经反编译出来了。

三、消息发送

消息发送的第一个流程,就是消息发送,这一步主要在类及其父类方法缓存以及方法列表中寻找是否有对应的方法。
首先,会根据消息接收者所属的类,查找类”方法列表“,若能找到与”选择子“名称相符的,即跳转其实现代码。否则,按接受者的继承体系继续向上查找,等找到合适的方法之后再跳转。
如果,最终未找到,那就执行动态方法解析的流程。
上面的过程会造成性能上的损失,鉴于此,objc_msgSend会在接受者第一次查找方法后,将该方法及其跳转地址缓存在哈希表中,每个类都有这样一块缓存,后面发送消息,会先哈希表搜寻,并实现快速跳转。当然,相对静态绑定这当然更慢。但是,实际上,这不会造成程序的性能瓶颈所在。
objc源码归纳出如下流程:
notion image
针对上面的流程,需要说明几点。

3.1 在方法列表中查找

在方法列表中查找方法,系统为了提高效率,做了如下区分:
  • 已排序的方法列表:二分查找
  • 未排序的方法列表:遍历

3.2 在父类方法列表中查找

如果在父类方法列表中查找到方法,那么就缓存到当前receiverClass中。

四、动态方法解析

假如在消息发送过程中,没有查找到方法,那么就会进入动态方法解析。
动态方法解析就是在运行时临时添加一个方法实现,来进行消息的处理。
添加方法的函数是:
对应的,下图是动态解析的一个流程:
notion image
动态方法解析的对应的示例代码

4.1 对象方法与类方法

动态方法解析,可以添加处理对象方法也可以处理类方法。
需注意区别的是,类方法,需要给其元类对象添加方法,而实例对象,是给其类对象添加方法。这也很好理解,因为:
  • 调用对象方法,查找方法是去类对象方法列表;
  • 调用类方法,是去元类方法列表中找;
如下,是一个示例:

4.2 class_addMethod

下面是class_addMethod添加的两种方式:
notion image

4.3 标记“已经动态解析”

这个标记有何作用?
因为动态解析之后,其实还是又重新走消息发送的阶段了。之所以加这个标记,是为了打破:
消息发送->动态方法解析->消息发送->动态方法解析....这个无限循环。
只会执行一次:消息发送->动态方法解析->消息发送->消息转发。

4.4 @dynamic的实现

动态方法解析,最佳的一个实践用例就是,@dynamic的实现。
@dynamic是告诉编译器不用自动生成getter和setter的实现,等到运行时再添加方法实现
notion image

五、消息转发

在消息发送——没有在缓存和方法列表中找到,也没有在动态方法解析时,添加方法。就会走到消息转发流程。
消息转发流程,分类两步:
  1. 寻找备援接收者
  1. 完整的消息转发
以下是流程图:
notion image

5.1 备援接收者

备援接收者,含义清晰,相当于“这条消息,我不想要接收,有个备份对象来接收”。
备援接收者,在下面方法实现:
在这一步,运行期系统会问它:是否把这条消息转给其他接收者来处理。
若方法能找到备援者对象,将其返回,否则返回nil
通过此方案,可以组合(composition)来模拟出多重继承(multiple inheritance)的某些特性。在一个对象内部,可能还有其他一系列对象,该对象可以经由此方法将能够处理某选择子的相关内部对象返回,如此一来,从外部看来,好像是该对象亲自来处理这些消息似的。
需要注意的是,这一步是不能改变消息内容的,如果要改变消息内容,就得通过完整的消息转发机制来做。
notion image

5.2. 完整的消息转发

在没有备援接收者的情况下,就会进入完整的消息转发流程中。
完整的消息转发,也分为两步:
  1. 获取方法签名:methodSignatureForSelector
  1. 进行转发:forwardInvocation
notion image

5.2.1 方法签名

方法签名,可以通过下面方式获取。
notion image

5.2.2 forwardInvocation

forwardInvocation方法,非常强大,可定制性程度极高,赋予了其极大的权限。
NSInvocation是一个封装了方法调用的类,把与尚未处理那条消息有关的全部细节都封于其中,此对象包含选择子目标(target)参数。在触发NSInvocation对象时,消息派发系统会将消息指派给目标对象。
当然,该方法也可以直接将消息转给备援接收者,但是在上一步中即可做到,所以一般到了这一步,都会修改消息内容,来做只有它能做的事。
但需要注意的时,在使用NSInvocation对象target时,targetassign类型。
notion image

5.3 类方法的消息转发

针对备援接收者及完整的消息转发流程,其实平时开发中,一般都认为只有对象方法可以实现消息转发
其实系统也支持对类方法的消息转发
只需要将智能提示后的对象方法前面-修改成+,即可实现类方法的消息转发
notion image

六、unrecognized selector

在历经千山万水之后,仍然走到这一步。
苍天绕过谁,那就抛出我们常见的错误吧!
  • [NSCFNumber lowercaseString]: unrecognized selector sent to instance 0x87** Terminating app due to uncaught exception 'NSInvalidArgumentException',reason:'- [NSCFNumber lowercaseString]: unrecognized selector sent to instance 0x87

参考

链接
  1. objc源码
  1. Hmmm, What's that Selector?
 
示例代码
  1. 01objc_msgSend
  1. 02动态方法解析
  1. 03消息转发- 备援接收者
  1. 03消息转发- 实例方法
  1. 03消息转发- 类方法
  • Objective-C
  • runtime
  • runtime(五)类的判定runtime(二)isa指针
    Loading...
    目录