Objective-C Runtime 包含两大组件——编译器和Runtime API,本篇探讨一下编译器组件扮演的角色和它的工作原理。
苹果官方文档中介绍了三种和 Runtime 交互的方式:
- Objective-C 源代码本身。
- 调用 NSObject 中的方法。
- 调用包含在 <objc/runtime.h>、<objc/message.h> 中的函数。
其中第一条“Objective-C 源代码本身”的含义就是,所有 Objective-C 源代码都是会被转换成 Runtime 代码运行,这个过程中开发者不用做任何事情,只要去编写 Objective-C 代码,编译,运行,这个过程中就自动在和 Runtime 进行交互了。
这也就是 Objective-C 的第一个组建——编译器做的事情,即把 Objective-C 代码转换为 Runtime 代码。
编译器
Runtime 编译器的作用就是把任何 Objective-C 的代码编译为 Runtime 的 C 代码。
这个过程中,会把 Objective-C 的数据结构编译为 Runtime 的 C 的数据结构。把 Objective-C 的消息传递编译为 Runtime 的 C 函数调用。
下面来看看 Objective-C 中的各个元素在 Runtime 中对应的是什么。
对象
Runtime 的核心数据结构就是对象,其实在 Objective-C 中,一切都是对象,对象在 Runtime 中是一个名为 objc_object 的结构体。Objective-C 中的任何对象都是用 objc_object 来表示的。
1 | struct objc_object { |
objc_object 中最关键的部分是一个 isa_t 类型的 isa 变量。isa_t 类型使用了 Tagged Pointer 技术来减小内存空间的占用。isa 指针主要保存了对象关联的类的信息。
id 的定义
1 | typedef struct objc_object *id; |
id 是一个指向 objc_object 类型的指针,因此 id 可以代表任何对象。
不透明类型
不透明类型是一种 C 语言结构体,它的声明中只暴露了部分信息,隐藏了它的具体实现细节。
类
1 | struct objc_class { |
类的 isa 指针指向它的元类。
Class 的定义
1 | typedef struct objc_class *Class; |
元类
元类中保存了这个类的类方法的地址。元类的 isa 指针指向元类对应的类的父类的元类。
方法
方法在 Runtime 中用 Method 类型来表示,从 Runtime 的源码可以看到,Method 类型是一个指向 method_t 结构体类型的指针。
1 | typedef struct method_t *Method; |
1 | struct method_t { |
实例变量
1 | typedef struct ivar_t *Ivar; |
1 | struct ivar_t { |
属性
1 | typedef struct property_t *objc_property_t; |
1 | struct property_t { |
协议
1 | struct protocol_t : objc_object { |
Objective-C 方法和隐含参数
对于每一个 Objective-C 的方法,例如:
1 | - (void)printCount:(NSInteger)count { |
编译器都会将其编译成一个 C 函数,上面的方法会被编译成:
1 | void foo(id self, SEL _cmd, int count) { |
这个 C 函数的第一个和第二个参数就是隐含参数,在 Objective-C 的方法体中,也是可以直接使用的。编译为 C 函数后,需要在函数声明中明确的声明。
消息传递
对于消息传递的代码:
1 | [receiver message]; |
编译器会把它编译成对 C 函数 objc_msgSend 的调用。
1 | objc_msgSend(receiver, @selector(message)); |
代码编译实例
因为 Objective-C 的源代码都会被编译成 Runtime 代码来运行,我们一样可以通过直接编写 Runtime 代码的方式来编写程序。
例如我们有个类叫 ClassA:
1 | @interface ClassA : NSObject |
然后来看看下面代码转成 Runtime 怎么写:
1 | ClassA *a = [[ClassA alloc] init]; |
结果如下:
1 | // 获取到 ClassA 的 Class 对象 |
输出结果:
1 | count = 100 |
使用 clang 来查看编译后的代码
可以使用 clang 命令来生成编译后的 Runtime 代码。
假设上面的例子的代码都写在 main.m 中,执行:
1 | clang -rewrite-objc main.m |
执行完毕后会在同一目录下生成一个 main.cpp 文件,打开它,搜索 int main() 可以看到:
1 | int main(int argc, const char * argv[]) { |
跟上面我们写的代码有点不同,但是可以看到生成的代码中也是大量使用了 objc_msgSend 的调用。