Runtime消息转发

OC调用方法被编译转化为如下的形式:

id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)

其实,除了objc_msgSend之外,消息发送的方法还有:objc_msgSend_stretobjc_msgSendSuperobjc_msgSendSuper_stret等,如果消息发送给超类就用带有super的方法,如果返回值是结构体而不是普通值就用带有stret的值。

运行时消息发送的详细步骤:
  • 1.检测selector是不是需要忽略的,比如Mac OS开发,有了垃圾回收机制,就不理会retain、release等这些函数了。

  • 2.检测target是否为nil,因为Objc的特性是允许对任何一个空对象发送方法而不会crash,因为会被忽略掉。

  • 3.如果上面两个都过了,就开始找这个类的IMP,先从cache中找,如果可以找到就直接跳到这个函数里去执行。

  • 4.如果cache列表里找不到,就找一下方法列表methodLists;

  • 5.如果类中methodLists找不到,就到超类的methodList中去找。一直找,直到找到NSObject类中为止。

  • 6.如果还找不到,Runtime提供了三种方法来处理:动态方法解析、消息接受者重定向、消息重定向。

这三种方法的调用关系如图:
消息转发流程.jpg
动态方法解析(resolveClassMethod)

所谓动态解析,可以理解为在methodLists中没有找到方法时,动态添加方法的一种策略。主要步骤如下:

// OC方法:
// 类方法未找到时调起,可于此添加类方法实现
+ (BOOL)resolveClassMethod:(SEL)sel
// 实例方法未找到时调起,可于此添加实例方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel

//  Runtime方法:
/**
 运行时方法:向指定类中添加特定方法实现的操作
 @param cls 被添加方法的类
 @param name selector方法名
 @param imp 指向实现方法的函数指针
 @param types imp函数实现的返回值和参数类型
 @return 添加方法是否成功
 */
 BOOL class_addMethod(Class _Nullable cls,
                      SEL _Nonnull name,
                      IMP _Nonnull imp,
                      const char * _Nullable types)

以一个具体的例子来说明,Person中声明了两个方法,一个实例方法、一个类方法,但是没有实现。然后使用runtime为它动态的添加方法的实现。

@interface MFPerson : NSObject

// 声明类方法,但未实现
+ (void)haveMeal:(NSString *)food;
// 声明实例方法,但未实现
- (void)singSong:(NSString *)name;

@end
#import "MFPerson.h"
#import <objc/runtime.h>

@implementation MFPerson

// 重写父类方法:处理类方法
+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(haveMeal:)) {
        Class class = objc_getMetaClass(object_getClassName(self));
        class_addMethod(class,
                        sel,
                        class_getMethodImplementation(class, @selector(mf_haveMeal:)),
                        "v@");
        return YES;
    }
    return [class_getSuperclass(self) resolveClassMethod:sel];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(singSong:)) {
        class_addMethod([self class],
                        sel,
                        class_getMethodImplementation(self, @selector(mf_singSong:)),
                        "v@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

+ (void)mf_haveMeal:(NSString *)food {
    NSLog(@"%s", __func__);
}

- (void)mf_singSong:(NSString *)name {
    NSLog(@"%s", __func__);
}
@end

测试代码:

// 测试:MFPerson类未实现类方法和实例方法,但是未崩溃
[MFPerson haveMeal:@"方便面"];
MFPerson *person = [[MFPerson alloc] init];
[person singSong:@"忽然"];

注意点:
1.class_addMethod中的特殊参数"v@",具体参考:https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100
2.成功使用动态方法解析还有个前提,就是必须存在可以处理消息的方法,比如上述例子中的:zs_haveMeal、zs_singSong。

消息接收者重定向(forwardingTargetForSelector)

前面方法动态解析的两个resolve方法都是返回BOOL值,当返回的是YES的时候即可正常执行,如果返回NO,消息发送机制就进行到了消息转发(Forwarding)阶段了,可以像下面这样,使用runtime更改消息接收者为其他对象,从而保证程序的正常运行。

// 重定向类方法的消息接受者,返回一个类
+ (id)forwardingTargetForSelector:(SEL)aSelector
// 重定向实例方法的消息接受者,返回一个实例对象
- (id)forwardingTargetForSelector:(SEL)aSelector

举例说明,先创建一个Student类,声明两个方法,同时实现。

@interface MFStudent : NSObject

// 类方法
+ (void)takeExam:(NSString *)exam;
// 实例方法
- (void)learnKnowledge:(NSString *)course;

@end

然后在TestViewController.m控制器中测试,先添加如下两个方法:

// 重定向类方法的消息接受者,返回一个类
+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(takeExam:)) {
        return [MFStudent class];
    }
    return [super forwardingTargetForSelector:aSelector];
}

// 重定向实例方法的消息接受者,返回一个实例对象
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(learnKnowledge:)) {
        return self.student;
    }
    return [super forwardingTargetForSelector:aSelector];
}

在viewDidLoad方法中:

[MessageForwardController performSelector:@selector(takeExam:)
                                   withObject:@"语文"];
self.student = [[MFStudent alloc] init];
[self performSelector:@selector(learnKnowledge:)
           withObject:@"天文学知识"];

结果:

+[MFStudent takeExam:]
-[MFStudent learnKnowledge:]
消息重定向(forwardInvocation)

当以上两个方法失效,那么这个时候该对象会因为找不到相应的方法而无法进行响应。此时,runtime会通过forwardInvocation:消息通知该对象,给予此次消息发送最后一次寻找IMP的机会:

- (void)forwardInvocation:(NSInvocation *)anInvocation;

其实每个对象都从NSObject中继承了forwardInvocation:方法,但是NSObject中的这个方法只是简单的调用了doesNotRecongnizeSelector:方法,提示我们错误。所以,我们也可以通过重写这个方法,对不能处理的消息进行一些默认的处理,也可以将消息转发给其他的对象来处理,而不是抛出错误。

我们注意到anInvocation是forwardInvocation的唯一参数,它封装了原始的消息和消息参数。正是因为它,我们还得重写一个方法:methodSignatureForSelector。这是因为在forwardInvocation消息发送之前,runtime系统会向对像发送methodSignatureForSelector消息,并取到返回的方法签名用于生成anInvocation。

下面使用一个示例来重新定义转发逻辑:在上面的TestViewController添加如下代码:

@interface MFStudent : NSObject

// 类方法
+ (void)takeExam:(NSString *)exam;
// 实例方法
- (void)learnKnowledge:(NSString *)course;

@end
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 1.从anInvocation中获取消息
    SEL sel = anInvocation.selector;
    // 2.判断student方法是否可以响应sel
    if ([self.student respondsToSelector:sel]) {
        // 2.1.若可以响应,则将消息转发给其他对象处理
        [anInvocation invokeWithTarget:self.student];
    } else {
        // 2.2.若仍无法响应,则报错:找不到响应方法
        [self doesNotRecognizeSelector:sel];
    }
}

/**
 需要从这个方法中获取的信息来创建NSInvocation对象,
 因此我们必须重写这个方法,为给定的selector一个合适的方法签名
 */
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    if (!signature) {
        signature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
    }
    return signature;
}
@implementation MessageForwardController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.student = [[MFStudent alloc] init];
    [self performSelector:@selector(learnKnowledge:)
               withObject:@"英语"];
}

结果:

 -[MFStudent learnKnowledge:]

总结:
1.从以上代码可以看出,forwardingTargetForSelector仅支持返回一个对象,而forwardInvocation则不同,可以转发给任意多个对象。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容