Objective-C Runtime(三):编译器

Objective-C Runtime 包含两大组件——编译器Runtime API,本篇探讨一下编译器组件扮演的角色和它的工作原理。

苹果官方文档中介绍了三种和 Runtime 交互的方式:

  1. Objective-C 源代码本身。
  2. 调用 NSObject 中的方法。
  3. 调用包含在 <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
2
3
4
5
6
struct objc_object {
private:
isa_t isa;
public:
// ...
}

objc_object 中最关键的部分是一个 isa_t 类型的 isa 变量。isa_t 类型使用了 Tagged Pointer 技术来减小内存空间的占用。isa 指针主要保存了对象关联的类的信息。

id 的定义

1
typedef struct objc_object *id;

id 是一个指向 objc_object 类型的指针,因此 id 可以代表任何对象。

不透明类型

不透明类型是一种 C 语言结构体,它的声明中只暴露了部分信息,隐藏了它的具体实现细节。

1
2
3
struct objc_class {
Class isa;
}

类的 isa 指针指向它的元类。

Class 的定义

1
typedef struct objc_class *Class;

元类

元类中保存了这个类的类方法的地址。元类的 isa 指针指向元类对应的类的父类的元类。

方法

方法在 Runtime 中用 Method 类型来表示,从 Runtime 的源码可以看到,Method 类型是一个指向 method_t 结构体类型的指针。

1
typedef struct method_t *Method;
1
2
3
4
5
6
7
struct method_t {
SEL name;
const char *types;
IMP imp;

// ...
};

实例变量

1
typedef struct ivar_t *Ivar;
1
2
3
4
5
6
7
struct ivar_t {
int32_t *offset;
const char *name;
const char *type;

// ...
};

属性

1
typedef struct property_t *objc_property_t;
1
2
3
4
struct property_t {
const char *name;
const char *attributes;
};

协议

1
2
3
4
5
6
7
8
9
10
11
struct protocol_t : objc_object {
const char *mangledName;
struct protocol_list_t *protocols;
method_list_t *instanceMethods;
method_list_t *classMethods;
method_list_t *optionalInstanceMethods;
method_list_t *optionalClassMethods;
property_list_t *instanceProperties;

// ...
};

Objective-C 方法和隐含参数

对于每一个 Objective-C 的方法,例如:

1
2
3
- (void)printCount:(NSInteger)count {
// ...
}

编译器都会将其编译成一个 C 函数,上面的方法会被编译成:

1
2
3
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
2
3
4
5
6
7
8
9
10
@interface ClassA : NSObject
@property (nonatomic, assign) NSInteger count;
- (void)printCount;
@end

@implementation ClassA
- (void)printCount {
NSLog(@"count = %@", @(self.count));
}
@end

然后来看看下面代码转成 Runtime 怎么写:

1
2
3
ClassA *a = [[ClassA alloc] init];
a.count = 100;
[a printCount];

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 获取到 ClassA 的 Class 对象
Class ClassA = objc_getClass("ClassA");

// 发送 alloc 和 init 消息来创建和初始化实例对象
id a = objc_msgSend(ClassA, @selector(alloc));
a = objc_msgSend(a, @selector(init));

// 获取到属性 count 背后的实例变量
Ivar countIvar = class_getInstanceVariable(ClassA, "_count");
assert(countIvar);

// 通过实例对象首地址 + 实例变量的地址偏移量得到实例变量的指针地址,然后通过 * 取指针值操作符修改指针指向的地址的值。
CFTypeRef aRef = CFBridgingRetain(a);
int *countIvarPtr = (int *)((uint8_t *)aRef + ivar_getOffset(countIvar));
*countIvarPtr = 100;
CFBridgingRelease(aRef);

// 给 a 对象发送 printCount 消息,打印 count 属性的值
objc_msgSend(a, @selector(printCount));

输出结果:

1
count = 100

使用 clang 来查看编译后的代码

可以使用 clang 命令来生成编译后的 Runtime 代码。

假设上面的例子的代码都写在 main.m 中,执行:

1
clang -rewrite-objc main.m

执行完毕后会在同一目录下生成一个 main.cpp 文件,打开它,搜索 int main() 可以看到:

1
2
3
4
5
6
7
8
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
ClassA *a = ((ClassA *(*)(id, SEL))(void *)objc_msgSend)((id)((ClassA *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("ClassA"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL, NSInteger))(void *)objc_msgSend)((id)a, sel_registerName("setCount:"), (NSInteger)100);
((void (*)(id, SEL))(void *)objc_msgSend)((id)a, sel_registerName("printCount"));
}
return 0;
}

跟上面我们写的代码有点不同,但是可以看到生成的代码中也是大量使用了 objc_msgSend 的调用。