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 函数。

消息发送流程

  1. 检查 selector 是否要忽略,比如自动引用计数下 retainrelease 方法将会被忽略。
  2. 检查 target 是否为 nil,如果为 nil 则直接返回 nil
  3. 通过 selector 查找具体的 IMP,首先查找缓存列表。具体方法就是通过一个哈希函数将 SEL 映射到指定的 bucket_t 在数组中的位置。
  4. 如果换成列表没有,开始查找方法列表。对于已排序好的列表,采用 二分查找法 进行查找。对于没有排序好的列表,就进行遍历查找。如果查找到了会将对应的 IMP 放入方法缓存列表中。
  5. 如果方法列表没有,逐级查找父类缓存列表和方法列表,最终直到查找 NSObject 类。
  6. 进入动态方法解析、消息转发流程。

消息转发

动态方法解析

如果给一个对象发送消息,该对象并没有对应的方法实现,那么程序不会直接崩溃,首先会进入动态方法解析的过程。

// 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 关键字到属性,系统不提供 settergetter 方法实现,就需要在动态方法解析中为其动态添加两个方法实现。

动态添加方法

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