Runtime之消息机制

818 阅读5分钟

一、简介

OC 是一门动态的语言,而 Runtime 是使用 OC 开发 iOS 应用的一个核心技术。OC 中很多动态的特性都是通过 Runtime 来实现的。

当一个类进行方法的调用时,本质是利用 Runtime 的消息机制发送一条消息,从而达到调用方法的目的,其底层就是调用了 objc_msgSend 函数。

想要发送一条消息给某个对象,最多会经历三个阶段,即消息发送动态方法解析消息转发。如果经历这三个阶段都没有找到该消息,那么程序在运行时就会抛出一个错误 :unrecognized selector sent to instance

二、消息发送

  1. 当我们给某个对象发送消息时,通过该对象的 isa 找到该对象的类对象。在类对象中的方法缓存中查找有没有该方法,如果有即进行调用。

  2. 如果在类对象的缓存中没有该方法,那就在该类对象存储方法的位置查找方法,如果有该方法,先将该方法加入类对象的方法缓存中去,然后调用该方法。

  3. 如果没有找到该方法,通过 superclass 指针找其父类,先查找父类的方法缓存中有没有该方法,如果有该方法那么就进行调用,如果缓存中没有该方法,那么就从父类的方法列表中查找方法,如果找到该方法,先缓存到类对象的方法缓存中,再进行调用

  4. 如果元类对象的父类还是没找到该方法,就找父类的父类依次类推,重复步骤3。如果最终都没有找到该方法就进行第二个阶段即动态方法解析。

三、动态方法解析

当消息发送阶段完成后仍然没有找到对应的方法,那么就会来到动态方法解析阶段。动态方法解析可以帮助我们拦截到消息发送阶段未实现的方法。在动态方法解析提供的方法中,利用 runtime 动态的添加某个方法。当消息发送阶段调用的方法没有找到的时候,来调用我们动态添加的方法。

下面以实例方法为例进行说明:

我们定义一个类,该类中只有方法的声明而没有实现。

@interface SomeClass : NSObject

- (void)foo;

@end

因为是实例方法,所以我们在 SomeClass 的实现文件中需要重写 resolveInstanceMethod 方法,在该方法中来实现动态方法解析。

假设当我们调用 foo 方法时候,因为 foo 方法未被实现,来到动态方法解析阶段,我们让其调用 bar 方法的实现。

- (void)bar {
    NSLog(@"%s", __func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    // 判断方法名是否为 foo
    if (sel == @selector(foo)) {
        
        // 获取被添加的方法
        Method method = class_getInstanceMethod(self, @selector(bar));
        // 获取方法实现
//        IMP imp = class_getMethodImplementation(self, @selector(bar));
        IMP imp = method_getImplementation(method);
        // 获取 TypeEncoding
        const char *types = method_getTypeEncoding(method);
        
        // 动态添加方法
        class_addMethod(self, sel, imp, types);
        
        return YES;
    }
    
    return [super resolveInstanceMethod:sel];
}
  • 利用 runtimeclass_addMethod 函数动态的添加方法
    • 第1个参数:如果是添加实例方法,传入类对象,如果是添加类方法传入元类对象
    • 第2个参数:被动态添加方法的方法名称
    • 第3个参数:动态添加方法的方法实现也就是函数地址
    • 第4个参数:动态添加的方法的参数和返回值的 typeEncoding

动态解析阶段添加完方法后,会重新执行一遍消息发送的流程。

如果没有实现动态方法解析,那么就会来到第三个阶段消息转发阶段

四、消息转发

当前2个阶段都没有找到对应的消息,最终就会来到消息转发阶段,我们还是以实例方法进行举例说明:

  • 首先会调用 - (id)forwardingTargetForSelector:(SEL)aSelector 方法,在该方法中进行消息的转发,将该消息转发到其他类,让其他类处理该消息。
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(foo)) {
        return [[OtherClass alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

// OtherClass
@interface OtherClass : NSObject

- (void)foo;

@end
  
@implementation OtherClass
- (void)foo {
    NSLog(@"%s", __func__);
}
@end
  • 创建一个 OtherClass 的类,该类中声明并实现了 foo 方法
  • forwardingTargetForSelector 方法中返回了 OtherClass 的实例对象,意味着我们会将消息交给 OtherClass 去处理,此时就会调用该类中的 foo 方法。

如果 forwardingTargetForSelector 方法返回 nil 或者压根就没有实现,消息转发阶段会调用其他方法来进行处理。

  • 首先调用的是 methodSignatureForSelector 方法,该方法返回一个 NSMethodSignature 对象,如果返回值不为 nil,那么他将会调用 forwardInvocation 方法,并且传入一个 NSInvocation 对象,该对象封装了方法调用所必须的条件。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    
    if (aSelector == @selector(foo)) {
        
        Method method = class_getInstanceMethod([OtherClass class], aSelector);
        const char *types = method_getTypeEncoding(method);
        NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:types];
        return signature;
    }
    
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
		[anInvocation invokeWithTarget:[[OtherClass alloc] init]];
  
  	// 假如 types 随便传的,anInvocation 封装的内容就毫无意义
  	// 我们可以随意进行任何操作
    OtherClass *oc = [[OtherClass alloc] init];
    [oc bar];
}
  • 如果我们想要用 anInvocation 来处理消息,那么在 methodSignatureForSelector 方法中 types 一定要传对,关于 types 如何表示的我们可以查看苹果的官方文档Type Encoding
  • 如果我们不想利用 anInvocation 处理消息,types 其实就可以随便传,那么我们就不能使用 anInvocation 来处理消息了,因为它封装的也是乱七八糟的东西。