Objective-C Runtime 之三 理解消息机制
ObjC 中的方法调用是一个传递消息的过程,它不是直接去调用一个函数,而是通过 selector 去查找 IMP 的过程,最终查找到函数指针,然后执行具体函数实现;如果没有找到函数实现,还会继续执行消息转发流程,最后仍没有查找到函数实现,就报错崩溃。
objc_msgSend 函数
// message.h
OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
ObjC 通常的方法调用实质上就是调用 objc_msgSend 函数。
如下代码:
Person *olivia = [[Person alloc] init];
[olivia sayHi];
使用命令 clang -rewrite-objc main.m 编译成 C++ 代码后如下:
Person *olivia = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)olivia, sel_registerName("sayHi"));
如果是使用 super 关键字,则调用的是 objc_msgSendSuper 函数。
消息发送流程
- 检查
selector是否要忽略,比如自动引用计数下retain、release方法将会被忽略。 - 检查
target是否为nil,如果为nil则直接返回nil。 - 通过
selector查找具体的IMP,首先查找缓存列表。具体方法就是通过一个哈希函数将SEL映射到指定的bucket_t在数组中的位置。 - 如果换成列表没有,开始查找方法列表。对于已排序好的列表,采用
二分查找法进行查找。对于没有排序好的列表,就进行遍历查找。如果查找到了会将对应的IMP放入方法缓存列表中。 - 如果方法列表没有,逐级查找父类缓存列表和方法列表,最终直到查找
NSObject类。 - 进入动态方法解析、消息转发流程。
消息转发
动态方法解析
如果给一个对象发送消息,该对象并没有对应的方法实现,那么程序不会直接崩溃,首先会进入动态方法解析的过程。
// NSObject.h
+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
动态方法解析核心是上述两个方法(都是 NSObject 的类方法),分别解决类方法和实例方法。当对象没有对应方法实现的时候,上述方法就是第一个补救措施。
对于 @dynamic 关键字到属性,系统不提供 setter 和 getter 方法实现,就需要在动态方法解析中为其动态添加两个方法实现。
动态添加方法
void playIMP(id self, SEL _cmd) {
NSLog(@"Game Start!");
}
void playWithTypeIMP(id self, SEL _cmd, GameType type) {
switch (type) {
case GameTypeOne:
NSLog(@"Game Type One Start!");
break;
case GameTypeTwo:
NSLog(@"Game Type Two Start!");
break;
case GameTypeThree:
NSLog(@"Game Type Three Start!");
break;
default:
break;
}
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(play)) {
class_addMethod(self, sel, (IMP)playIMP, "v@:");
return YES;
} else if (sel == @selector(playWith:)) {
class_addMethod(self, sel, (IMP)playWithTypeIMP, "v@:i");
return YES;
}
return [super resolveInstanceMethod:sel];
}
在 resolveInstanceMethod 方法中,使用 class_addMethod 函数为指定的类动态添加方法。
消息重定向
如果 resolveInstanceMethod: 方法中没有找到方法实现,将会进入第二个补救措施,就是消息重定向 forwardingTargetForSelector:,这个方法可以将消息的接受者更换为其他对象。
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(play)) {
return [SecondGame new]; // 返回一个其他的对象,如果这个对象存在这个方法实现的话
}
return nil;
}
转发
如果上述方法返回 nil,则会进入到最后一个补救方法中,即消息转发流程。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(play)) {
// 这里返回方法签名
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
// 上面是直接写,下面是返回一个方法的 types,都可以
// return [self methodSignatureForSelector:@selector(anotherPlay)];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
// 使用另一个方法实现做代替
if (anInvocation.selector == @selector(play)) {
anInvocation.selector = @selector(anotherPlay);
// 也可以是其他类,看实际情况。
[anInvocation invokeWithTarget:self];
}
}
最终这里可以实现,让原本的消息接受者去执行一个其他的方法,也可以更好消息接受者,去执行一个指定的方法,都是可以的。
模拟继承
使用消息转发的原理,可以实现模拟多继承,我们定义一个 Person 类和一个 Machine 类,并定义一个 Robot 类对前两个类进行继承。
// Person
@interface Person : NSObject
- (void)hello;
@end
@implementation Person
- (void)hello {
NSLog(@"Hello World!");
}
@end
// Machine
@interface Machine : NSObject
- (void)lubricate;
@end
@implementation Machine
- (void)lubricate {
NSLog(@"Lubricate.");
}
// Robot
@interface Robot : NSObject
- (void)hello;
- (void)lubricate;
@end
#import "Robot.h"
#import "Person.h"
#import "Machine.h"
@interface Robot ()
@property (nonatomic, strong) Person *person;
@property (nonatomic, strong) Machine *machine;
@end
@implementation Robot
- (instancetype)init {
self = [super init];
if (self) {
self.person = [[Person alloc] init];
self.machine = [[Machine alloc] init];
}
return self;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
if ([self.person respondsToSelector:aSelector]) {
return self.person;
} else if ([self.machine respondsToSelector:aSelector]) {
return self.machine;
} else {
return [super forwardingTargetForSelector:aSelector];
}
}
@end