Objective-C Runtime(二):动态类型,动态绑定,动态方法决议,内省

本文介绍了 Objective-C Runtime 的四个重要的概念——动态类型,动态绑定,动态方法决议,内省,为后面的研究打下基础。

动态类型

动态类型(Dynamic typing)是指对象的具体类型在运行时才能确定。

在 Objective-C 中,对象的类型可以定义成静态的,也可以定义成动态的。

下面代码中的 number 的类型被定义为 NSNumber *,它是静态类型。

1
NSNumber *number;

动态类型一般用 id 关键字来定义:

1
id object;

这个 object 可以是任何类型。

然而静态类型的作用是告诉编译器做类型检查,保证在代码中不会出现类型不匹配的赋值等操作。实际上定义为静态类型的对象也可能是动态类型。在实际开发中还是会出现类似于定义为 NSNumber 的类型实际上保存的值是一个 NSString 的现象,需要小心谨慎。

动态类型的存在可以让代码更加灵活。

比如一个方法的某个参数支持传入多种类型,有了 id 这个类型,这个方法就只要声明一次,而不用对于每种参数类型都声明一个方法,从而减少方法的数量。例如:

1
- (NSInteger)computeValue:(id)parameter;

Objective-C 也支持定义为符合某个协议的任意类型:

1
- (NSInteger)computeValue:(id<NSDecimalNumberBehaviors>)parameter;

或者是类型为某个类或其子类,同时符合某个协议的任意类型:

1
- (NSInteger)computeValue:(NSNumber<NSDecimalNumberBehaviors> *)parameter;

动态绑定

动态绑定(Dynamic binding)是指把消息映射到方法实现的这一过程是在运行时,而不是在编译时完成的。

例如下面代码中:

1
2
id computer = [[Computer alloc] initWithBrand:@"Dell"];
[computer logInfo];

可能有多个类都实现了 logInfo 方法,被声明为 id 类型的 computer 对象也可能是任意具体类型。当 logInfo 消息发送给 computer 对象时,Runtime 会先去决定 computer 的实际类型,然后用 logInfo 的 Selector 在 computer 的实际类型中去寻找对应的方法实现,如果 computer 的类型中找不到,还会继续去它的父类中找。

Objective-C 对于消息传递(方法调用)的处理可以简化为下面三个步骤:

  1. 决定消息接受者的类型(动态类型)
  2. 决定消息对应的方法实现(动态绑定)
  3. 调用方法

动态方法决议

动态方法决议(Dynamic Method Resolution)是一种能为方法动态的提供方法实现的能力。

Objective-C 的 @dynamic 关键字就属于动态方法决议,这个关键字告诉编译器这个属性的 setter 和 getter 方法是在运行时动态提供的。例如 Core Data 框架中就大量使用了 @dynamic 来实现更高效的数据访问。

NSObject 包含两个类方法:

1
+ resolveInstanceMethod:

resolveInstanceMethod 为给定的 Selector 提供一个实例方法的实现。这个方法会在每次实例对象收到消息时调用。

1
+ resolveClassMethod

resolveClassMethod 为给定的 Selector 给提供一个类方法的实现。这个方法会在每次类收到消息时调用。

复写这两个方法即可在运行时为实例方法或类方法动态的提供实现。

举个例子:

有个空的 Bird 类

1
2
3
4
5
6
7
// Bird.h
@interface Bird : NSObject
@end

// Bird.m
@implementation Bird
@end

创建一个 Bird 的实例,给它发送一个 fly 消息

1
2
3
// main.m
Bird *bird = [[Bird alloc] init];
[bird performSelector:NSSelectorFromString(@"fly")];

由于 fly 消息没有被声明,只能用 performSelector:NSSelectorFromString ... 这种方式在运行时发送消息。

当然这时程序是会崩溃的,因为 Bird 类并没有 fly 方法,因此 bird 对象无法处理这个消息。

现在在 Bird.m 的头部添加名为 fly 的 C 函数:

1
2
3
4
// Bird.m
void fly(id self, SEL _cmd) {
NSLog(@"Bird are flying...");
}

然后为 Bird 添加 resolveInstanceMethod: 方法:

1
2
3
4
5
6
7
8
9
10
// Bird.m
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *method = NSStringFromSelector(sel); // 1
if ([method isEqualToString:@"fly"]) {
class_addMethod([self class], sel, (IMP)fly, "v@:"); // 2
return YES; // 3
}

return [super resolveInstanceMethod:sel];
}

以上代码执行步骤如下:

  1. 取得表示指定 Selector 的字符串。
  2. 如果字符串为 “fly”,为 Bird 类添加 fly 方法,方法的实现为上面定义的 fly 函数。
  3. 返回 YES 表示已经自己正确的处理好了这个方法实现的提供。

运行程序,当 bird 对象收到 fly 消息时,会触发 resolveInstanceMethod: 的调用,这里会为 Bird 类动态地添加 fly 方法,然后 bird 对象会继续处理 fly 消息,这时就能正确的调用了,因为找到了 fly 方法的实现,会在控制台打印出 “Bird are flying…”。

注:如果不明白 fly 函数中 id self, SEL _cmd 的作用,class_addMethod 方法的使用,”v@:” 是什么等等,后面的文章会有对这些方面的介绍。

内省

NSObject API 中提供了对象内省(Introspection)的功能。内省是一种能在运行时检查对象自身信息的能力。

由于 Objective-C 的大量行为都是在运行时完成的,内省能力便至关重要。

NSObject 对象提供的内省方法主要有:

isKindOfClass:
检查对象是否是给定的 Class 的实例或给定 Class 的子类的实例。

respondsToSelector:
检查对象是否能响应某个 Selector。

conformsToProtocol:
检查对象是否符合某个协议。

methodSignatureForSelector:
获取某个 Selector 得方法签名。

如果一个方法用动态类型 id 作为参数,例如前面提到的:

1
- (NSInteger)computeValue:(id)parameter;

通常要对这个参数进行内省检查来决定进一步操作。