从源代码看 ObjC 中消息的发送

459 查看

Blog: Draveness

关注仓库,及时获得更新:iOS-Source-Code-Analyze

因为 ObjC 的 runtime 只能在 Mac OS 下才能编译,所以文章中的代码都是在 Mac OS,也就是 x86_64 架构下运行的,对于在 arm64 中运行的代码会特别说明。

写在前面

如果你点开这篇文章,相信你对 Objective-C 比较熟悉,并且有多年使用 Objective-C 编程的经验,这篇文章会假设你知道:

  1. 在 Objective-C 中的“方法调用”其实应该叫做消息传递
  2. [receiver message] 会被翻译为 objc_msgSend(receiver, @selector(message))
  3. 在消息的响应链中可能会调用 - resolveInstanceMethod: 或者 - forwardInvocation: 等方法
  4. 关于选择子 SEL 的知识

    如果对于上述的知识不够了解,可以看一下这篇文章 Objective-C Runtime,但是其中关于 objc_class 的结构体的代码已经过时了,不过不影响阅读以及理解。

  5. 方法在内存中存储的位置,深入解析 ObjC 中方法的结构

    文章中不会刻意区别方法和函数、消息传递和方法调用之间的区别。

  6. 能翻墙(会有一个 Youtube 的链接)

概述

关于 Objective-C 中的消息传递的文章真的是太多了,而这篇文章又与其它文章有什么不同呢?

由于这个系列的文章都是对 Objective-C 源代码的分析,所以会从 Objective-C 源代码中分析并合理地推测一些关于消息传递的问题

objc-message-core

关于 @selector() 你需要知道的

因为在 Objective-C 中,所有的消息传递中的“消息“都会被转换成一个 selector 作为 objc_msgSend 函数的参数:

这里面使用 @selector(hello) 生成的选择子 SEL 是这一节中关注的重点。

我们需要预先解决的问题是:使用 @selector(hello) 生成的选择子,是否会因为类的不同而不同?各位读者可以自己思考一下。

先放出结论:使用 @selector() 生成的选择子不会因为类的不同而改变,其内存地址在编译期间就已经确定了。也就是说向不同的类发送相同的消息时,其生成的选择子是完全相同的

接下来,我们开始验证这一结论的正确性,这是程序主要包含的代码:

在主函数任意位置打一个断点, 比如 -> [object hello]; 这里,然后在 lldb 中输入:
objc-message-selecto

这里面我们打印了两个选择子的地址@selector(hello) 以及 @selector(undefined_hello_method),需要注意的是:

@selector(hello) 是在编译期间就声明的选择子,而后者在编译期间并不存在,undefined_hello_method 选择子由于是在运行时生成的,所以内存地址明显比 hello 大很多

如果我们修改程序的代码:
objc-message-selector-undefined

在这里,由于我们在代码中显示地写出了 @selector(undefined_hello_method),所以在 lldb 中再次打印这个 sel 内存地址跟之前相比有了很大的改变。

更重要的是,我没有通过指针的操作来获取 hello 选择子的内存地址,而只是通过 @selector(hello) 就可以返回一个选择子。

从上面的这些现象,可以推断出选择子有以下的特性:

  1. Objective-C 为我们维护了一个巨大的选择子表
  2. 在使用 @selector() 时会从这个选择子表中根据选择子的名字查找对应的 SEL。如果没有找到,则会生成一个 SEL 并添加到表中
  3. 在编译期间会扫描全部的头文件和实现文件将其中的方法以及使用 @selector() 生成的选择子加入到选择子表中

在运行时初始化之前,打印 hello 选择子的的内存地址:
objc-message-find-selector-before-init

message.h 文件

Objective-C 中 objc_msgSend 的实现并没有开源,它只存在于 message.h 这个头文件中。

在这个头文件的注释中对消息发送的一系列方法解释得非常清楚:

当编译器遇到一个方法调用时,它会将方法的调用翻译成以下函数中的一个 objc_msgSendobjc_msgSend_stretobjc_msgSendSuperobjc_msgSendSuper_stret。 发送给对象的父类的消息会使用 objc_msgSendSuper 有数据结构作为返回值的方法会使用 objc_msgSendSuper_stretobjc_msgSend_stret 其它的消息都是使用 objc_msgSend 发送的

在这篇文章中,我们只会对消息发送的过程进行分析,而不会对上述消息发送方法的区别进行分析,默认都使用 objc_msgSend 函数。

objc_msgSend 调用栈

这一小节会以向 XXObject 的实例发送 hello 消息为例,在 Xcode 中观察整个消息发送的过程中调用栈的变化,再来看一下程序的代码: