Objective-C--runtime整理

消息传递

动态绑定

在 Objective-C 中,方法的调用常被称作消息传递,这是因为 objc 是一种动态语言,有别于 C 语言。先来理解以下 C 语言的函数调用方式。C 语言使用“静态绑定”(static binding),也就是说,在编译期就能决定运行时所应调用的函数。以下列代码为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#import <stdio.h>
void printHello() {
printf("Hello, world!\n");
}
void printGoodbye() {
printf("Goodbye, world!\n");
}
void doTheThing(int type) {
if (type == 0) {
printHello();
} else {
printGoodbye();
}
return 0;
}

如果不考虑“内联”(inline),那么编译器在编译代码的时候就已经知道程序中有 printHello 与 printGoodbye 这两个函数了,于是会直接生成调用这些函数的指令。而函数地址实际上是硬编码在指令之中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#import <stdio.h>
void printHello() {
printf("Hello, world!\n");
}
void printGoodbye() {
printf("Goodbye, world!\n");
}
void doTheThing(int type) {
void (*fnc)();
if (type == 0) {
fnc = printHello;
} else {
fnc = printGoodbye;
}
fnc();
return 0;
}

这时就得使用“动态绑定”(dynamic binding)了。

objc_msgSend

在 objc 中,向对象传递消息时就会使用动态绑定机制来决定需要调用的方法。所有方法的底层都是普通的 C 语言函数,如:

1
id returnValue = [someObject messageName:parameter];

其中,messageName 叫做“选择子”(selector),选择子与参数合起来称为“消息”。编译器会将这条消息转换成一条标准的 C 语言函数调用:

1
void objc_msgSend(id self, SEL cmd, ...)

objc_msgSend是消息传递机制中的核心函数。例子中的消息转换为如下函数:

1
id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);

objc_msgSend函数会在接受者所属的类中搜寻其“方法列表”,如果能找到对应的方法,就跳至其实现代码;否则就沿着继承体系继续向上查找。如果最终都没有找到对应的方法,那就执行“消息转发”操作,详见下一节。

但是,如果每一次方法调用都要搜索一遍方法列表,效率会很低。因此 objc_msgSend 会将匹配结果缓存在“快速映射表”中,每个类都有这样一块缓存,提高了消息传递效率。

每个类中都有一张表格,记录着函数实现的指针,而选择子则作为键。消息传递就通过查找这张表来跳至实现代码。在跳转时采用了“尾调用优化”,这样就避免了为 objc_msgSend 函数准备额外的栈帧。

消息传递还需要用到其他一些函数:

  • objc_msgSend_stret:处理返回结构体
  • objc_msgSend_fpret:处理返回浮点数
  • objc_msgSendSuper:负责给超类发送消息,详见后文

向 nil 发送消息

向 nil 发送消息时,objc_msgSend 函数的第一个参数为 nil,在找寻其 isa 指针时就是0地址返回了,所以不会出现错误。

  • 如果返回值是一个对象,那么发送给 nil 的返回值也是 nil
  • 如果返回值是指针类型,发送给 nil 的返回值为0
  • 如果返回值是结构体,发送给 nil 返回的结构体的各字段值都是0
  • 返回值是其他,发送给 nil 的返回值将是未定义的

消息转发

当对象无法处理收到的消息时,即objc_msgSend函数没有找到处理消息的方法,就会用_objc_msgForward函数指针代替IMP,执行这个 IMP。启动所谓的“消息转发”机制。

消息转发分为两大阶段。第一阶段是征询接收者,能否给其动态添加方法,以处理当前这个“未知的选择子”,这叫做“动态方法解析”;第二阶段涉及“完整的消息转发机制”。第二阶段又分为两小步,首先,请接收者看看有没有其他对象能处理这条消息。若没有再启动完整的消息转发机制。

动态方法解析

首先将调用下列类方法:

1
+ (BOOL)resolveInstanceMethod:(SEL)selector

参数是未知的选择子,返回值表示是否能新增一个实例方法来处理该选择子。若是类方法,则叫做resolveClassMethod

class_addMethod给类动态添加方法。

备援接收者

1
- (id)forwardingTargetForSelector:(SEL)selector

通过该方法返回一个备援对象。

通过这个方法,也可以模拟出“多重继承”的某些特性。在一个对象内部,可能还有一系列其他对象,该对象可经由此方法将能够处理相关消息的内部对象返回。

完整的消息转发

首先会创建NSInvocation对象,把与尚未处理的那条消息有关的全部细节都封于其中,包括选择子、目标及参数。接着会调用以下方法:

1
- (void)forwardInvocation:(NSInvocation *)invocation

实现此方法,可以只改变调用目标,使消息在新目标上调用即可,但这样就和“备援接收者”等效了。所以比较有用的实现方式为:在触发消息前,先改变消息内容,如追加一个参数,或改换选择子等。

Method Swizzling(详见 EOC P52)

方法调配(method swizzling)能够在运行时改变给定选择子相对应的方法。在上述的选择子映射表中,选择子的名称所映射的方法实现是以函数指针来表示的,这种指针叫做 IMP,其原型如下:

1
id (*IMP)(id, SEL, ...)

在运行时,开发者能够向表中新增选择子,也可以改变某选择子所对应的方法实现,或者交换两个选择子所映射到的指针。想互换两个已经写好的方法实现,可用以下函数:

1
void method_exchangeImplementations(Method m1, Method m2)

两个参数表示待交换的方法实现,方法实现可用下列函数获得:

1
Method class_getInstanceMethod(Class aClass, SEL aSelector)

在实际开发中,方法调配一般用来调试黑盒方法,如让黑盒方法打印 log。

1
2
3
4
5
6
7
@implementation NSString (EOCMyAdditions)
- (NSString *)eoc_myLowercaseString {
NSString *lowercase = [self eoc_myLowercaseString];
NSLog(@"%@ => %@", self, lowercase);
return lowercase;
}
@end

然后交换:

1
2
3
Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(eoc_myLowercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);

这样,可以为那些“完全不知道具体实现”的黑盒方法增加日志记录功能,有助于程序调试。

self 与 super

1
2
3
4
5
6
7
8
9
10
11
@implementation Son : Father
- (id)init
{
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end

这段代码输出的都将是Son

self 是指向当前实例的指针,然而 super 却不是指向父类的指针。其实 super 本质是一个编译器标识符,和 self 指向同一个消息接收者。区别在于:super 会告诉编译器,调用 class 方法时,要去父类中找,而不是本类。

因此,不推荐在 init 方法中使用点语法。如果想访问实例变量应该使用下划线,而非点语法。点语法的坏处就是子类有可能覆写 setter。

在调用[super class]时,会转化成objc_msgSendSuper函数。函数定义:

1
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)

第一个参数objc_super是一个结构体:

1
2
3
4
struct objc_super {
__unsafe_unretained id receiver;
__unsafe_unretained Class super_class;
};

第一个成员相当于objc_msgSend中的 self,第二个成员说明当前类的父类是什么。

关联对象

关联对象指一个对象可以通过一个 key 连接到另一个对象上,即一个对象关联到另一个对象。可以用来动态给一个类增加实例,分类实现实例的添加也是通过这种方法。

1
2
3
4
5
6
//关联对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//获取关联的对象
id objc_getAssociatedObject(id object, const void *key)
//移除关联的对象
void objc_removeAssociatedObjects(id object)

其中,objc_AssociationPolicy的枚举值有:

1
2
3
4
5
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403

当对象被释放时,会根据这个策略来决定是否释放关联的对象。

设置关联对象时用的键是个“不透明的指针”(opaque pointer)。如果在两个键上调用isEqual:方法的返回值是 YES,那么 NSDictionary 就认为二者相等;然而在设置关联对象值时,若想令两个键匹配到同一个值,则二者必须是完全相同的指针才行。鉴于此,在设置关联对象值时,通常使用静态全局变量做键,用@selector作为 key 是比较好的选择。给分类实现属性一般可以这么写:

1
2
3
4
5
6
7
- (NSString *)associatedObject_retain {
return objc_getAssociatedObject(self, _cmd);
}
- (void)setAssociatedObject_retain:(NSString *)associatedObject_retain {
objc_setAssociatedObject(self, @selector(associatedObject_retain), associatedObject_retain, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
  1. AssociationsManager是顶级的对象,维护了一个从spinlock_t锁到AssociationsHashMap哈希表的单例键值对映射;
  2. AssociationsHashMap是一个无序的哈希表,维护了从对象地址到ObjectAssociationMap的映射;
  3. ObjectAssociationMap是一个C++中的map,维护了从keyObjcAssociation的映射,即关联记录;
  4. ObjcAssociation是一个C++的类,表示一个具体的关联结构,主要包括两个实例变量_policy表示关联策略_value表示关联对象。

关联对象与被关联对象本身的存储并没有直接的关系,它是存储在单独的哈希表中的。

此外,关联对象的生命周期在 ARC 与 MRC 下都不用手动管理。

参考资料