美文网首页
runtime - 理解OC的消息和消息转发机制

runtime - 理解OC的消息和消息转发机制

作者: SPIREJ | 来源:发表于2019-11-05 21:06 被阅读0次

    您将了解到了runtime是如何通过objc_msgSend在运行时把方法和方法实现进行动态绑定的;
    也将了解到runtime下动态方法解析和消息转发的机制是怎样的。

    消息

    本章描述了代码的消息表达式如何转换为对objc_msgSend函数的调用,如何通过名字来指定一个方法,以及如何使用objc_msgSend函数。

    获得方法地址

    避免动态绑定的唯一办法就是取得方法的地址,并且直接象函数调用一样调用它。
    当一个方法会被连续调用很多次,而且您希望节省每次调用方法都要发送消息的开销时,使用方法地址来调用方法就显得很有效。
    利用NSObject类中的methodForSelector:方法,您可以获得一个指向方法实现的指针,并可以使用该指针直接调用方法实现。methodForSelector:返回的指针和赋值的变量类型必须完全一致,包括方法的参数类型和返回值类型都在类型识别的考虑范围中。

    下面的例子展示了怎么使用指针来调用setFilled:的方法实现:

    void (*setter)(id, SEL, BOOL);
    int i;
    
    setter = (void (*)(id, SEL, BOOL))[target
    methodForSelector:@selector(setFilled:)];
    
    for ( i = 0; i < 1000, i++ )
     setter(targetList[i], @selector(setFilled:), YES);
    

    方法指针的第一个参数是接收消息的对象(self),第二个参数是方法选标(_cmd)。这两个参数在方法中是隐藏参数,但使用函数的形式来调用方法时必须显示的给出。
    使用methodForSelector:来避免动态绑定将减少大部分消息的开销,但是这只有在指定的消息被重复发送很多次时才有意义,例如上面的 for 循环。
    注意,methodForSelector:是 Cocoa 运行时系统的提供的功能,而不是 Objective-C 语言本身的功 能。

    objc_msgSend

    在objective-C中,消息时知道运行时才会与方法实现进行绑定的。编译器会把一个消息表达式:

    [receiver message]
    

    转换成一个对消息函数objc_msgSend的调用。该函数有两个主要参数:消息接收者和消息对应的方法名字---即方法选标。

    objc_msgSend(receive, selector)
    

    同时接收消息中的任意数目的参数:

    objc_msgSend(receive, select, arg1, arg2, ...)
    

    该消息函数做了动态绑定所需要的一切:
    它首先找到选标所对应的方法实线。因为不同的类对同一方法可能会有不同的实现,所以找到的方法实线依赖于消息接收者的类型。
    然后将消息接受者对象(指向消息接受者对象的指针)以及方法中指定的参数传给找到的方法实现。
    最后,将方法实现的返回值作为该函数的返回值返回。

    注意:objc_msgSend方法看起来好像返回了数据,其实objc_msgSend从不返回数据,而是你的方法在运行时方法实现被调用后才会返回数据。下面详细叙述消息发送的步骤(如下图):

    消息框架:


    消息框架

    这样就能解释objc_msgSend工作原理了,当对象收到消息时,为了匹配消息的接收者和选择子:

    1. 消息函数首先根据对象的isa指针找到该对象所对应的类的方法列表objc_method_list,并从方法列表中寻找该消息对应的方法选标。如果能找到就可以直接跳转到相关的具体实现中去调用。
    2. 如果找不到,将会通过super_class指针沿着继承树向上去搜索,直到继承树根部(通常为NSObject类)。一旦找到了方法选标,objc_msgSend则以消息接收者对象为参数调用,调用该选标对应的方法实现。
    3. 如果到了继承树根部还没有找到,就会进行消息转发,还有三次机会来处理。(消息转发在下文有介绍)

    这就是在运行时系统中选择方法实现的方式。在面向对象编程中,一般称作方法和消息动态绑定的过程

    为了加快消息的处理过程,运行时系统通常会将使用过的方法选标和方法实现的地址放入缓存中。每个类
    都有一个独立的缓存,同时包括继承的方法和在该类中定义的方法。消息函数会首先检查消息接收者对象
    对应的类的缓存(理论上,如果一个方法被使用过一次,那么它很可能被再次使用)。如果在缓存中已经
    有了需要的方法选标,则消息仅仅比函数调用慢一点点。如果程序运行了足够长的时间,几乎每个消息都
    能在缓存中找到方法实现。程序运行时,缓存也将随着新的消息的增加而增加。

    使用隐藏的参数

    疑问:
    我们经常用到关键字self,但是self是如何获取当前方法的对象呢?

    其实,这也是runtime系统的作用,self是在方法运行时被动态传入的。

    objc_msgSend找到方法对应实现时,它将直接调用该方法实现,并将消息中所有参数都传递给方法实现,同时,她还将传递两个隐藏参数

    • 接收消息的对象(self所指向的内容,当前方法的对象指针)
    • 方法选择器(_cmd指向的内容,当前方法的SEL指针)

    这些参数帮助方法实现获得了消息表达式的信息。它们被认为是“隐藏”的是因为它们并没有在定义方法的源代码中声明,而是在代码编译时插入方法实现中的。尽管这些参数没有被明确声明,在源代码中我们仍然可以引用它们。

    这两个参数中,self更实用。它是在方法实现中访问消息接收者对象的实例变量的途径。

    这时我们可能会想到另一个关键字super,实际上super关键字接收到消息时,编译器会创建一个objc_super结构体:

    struct objc_super { id receiver; Class class;}
    

    这个结构体指明了消息应该被传递给特定的父类。receiver仍然是self本身,当我们想通过[super class]获取父类时,编译器其实是将指向selfid指针和classSEL传递给objc_msgSendSuper函数。只有在NSObject类中才能找到class方法,然后class方法底层被转换为object_getClass(),接着底层编译器将代码转换为objc_msgSend(objc_super->receiver, @selector(class)),传入的第一个参数是指向selfid指针,与调用[self class]相同,所以我们得到的永远都是self的类型。因此你会发现:

    // 这句话并不能获取父类的类型,只能获取当前类的类型名
    NSLog(@"%@", NSStringFromClass([super class]));
    

    消息转发

    消息转发机制基本分为三个步骤:
    1、动态方法解析
    2、备用接收者
    3、完整转发

    整个消息转发流程如下图所示:

    image

    1、所属类动态方法解析

    首先,如果沿着继承树没有搜索到相关方法则会向接受者所属的类进行一次请求,调用所属类的类方法 +resolveInstanceMethod:(实例方法) 或者 +resolveClassMethod:(类方法)。在这个方法中,我们有机会为该未知消息新增一个”处理方法“。不过使用该方法的前提是我们已经实现了该”处理方法”,只需要在运行时通过class_addMethod函数动态添加到类里面就可以了。

    + (BOOL)resolveInstanceMethod:(SEL)sel;
    + (BOOL)resolveClassMethod:(SEL)sel;
    

    举个例子:

    // Person.m
    #import "Person.h"
    #import <objc/runtime.h>
    
    @interface Person()
    @property (nonatomic, copy) NSString *name;
    @property (nonatomic, assign) NSUInteger age;
    @end
    
    @implementation Person
    
    - (instancetype)init {
        if (self = [super init]) {
        }
        return self;
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        NSLog(@"resolveInstanceMethod: %@", NSStringFromSelector(sel));
        if (sel == @selector(appendString:)) {
            class_addMethod([self class], sel, (IMP)dynamicAdditonMethodIMP, "v@:");
            return YES;
        }
        return [super resolveInstanceMethod:sel];
    }
    
    + (BOOL)resolveClassMethod:(SEL)sel {
        NSLog(@"resolveClassMethod: %@", NSStringFromSelector(sel));
        return [super resolveClassMethod:sel];
    }
    
    void dynamicAdditonMethodIMP(id self, SEL _cmd) {
        NSLog(@"dynamicAdditonMethodIMP");
    }
    @end
    
    
    // viewController.m
    id *p = [[Person alloc] init];
    [p appendString:@""];
    

    输出结果:

    2018-06-12 14:38:54.461050+0800 getIP[12036:607027] resolveInstanceMethod: appendString:
    2018-06-12 14:38:54.461230+0800 getIP[12036:607027] dynamicAdditonMethodIMP
    

    首先创建了一个Person的实例对象,一定要用id类型来声明,否则会在编译器就报错,因为找不到相关函数的声明(这里是appendString:)。id类型由于可以指向任何类型的对象,因此编译时能够找到NSString类的相关方法声明就不会报错。

    由于Person类没有声明和定义appendString:方法,所以运行时应该会报unrecognized selector错误,但是并没有,因为我们重写了类方法+ (BOOL)resolveInstanceMethod:(SEL)sel,当找不到相关实例方法的时候就会调用该类方法去询问是否可以动态添加,如果返回YES就会再次执行相关方法,如何给一个类动态添加一个方法,那就是调用runtime库中的class_addMethod方法,该方法原型是:

    BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
    

    第一个参数是需要添加方法的类;
    第二个参数是一个selector,也就是实例方法的名字;
    第三个参数是一个IMP类型的变量也是函数实现,需要传入一个C函数,这个函数至少两个参数,一个是id self 一个是SEL _cmd
    第四个参数是函数类型,更多含义见:Type Encodings

    2、备用接收者

    动态方法解析无法处理消息时,则会走备用接收者。这个备用接收者只能是一个新的对象,不能是self本身,否则就会出现无线循环。如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。

    Person类声明两个方法:

    @interface Person : NSObject
    - (void)hello;
    + (Person *)hi;
    @end
    

    实现在Person.m中实现新的接收对象_helper和forwardingTargetForSelector:方法:

    
    @interface Person()
    {
        RuntimeMethodHelper *_helper;
    }
    @end
    
    
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        NSLog(@"forwardingTargetForSelector");
        NSString *selectorString = NSStringFromSelector(aSelector);
        // 将消息交给_helper来处理
        if ([selectorString isEqualToString:@"hello"]) {
            return _helper;
        }
        return [super forwardingTargetForSelector:aSelector];
    }
    

    RuntimeMethodHelper类需要实现转发的方法:

    #import "RuntimeMethodHelper.h"
    
    @implementation RuntimeMethodHelper
    
    - (void)hello {
        NSLog(@"%@, %p", self, _cmd);
    }
    
    @end
    

    最后在viewController.m中调用:

    id p = [[Person alloc] init];
    [p hello];
    

    输出结果:

    2018-06-12 16:54:26.113808+0800 getIP[13842:768645] forwardingTargetForSelector
    2018-06-12 16:54:26.114031+0800 getIP[13842:768645] <RuntimeMethodHelper: 0x60400000e270>, 0x10ed5f93b
    

    3、消息重定向

    如果动态方法解析和备用接收者都没有处理这个消息,就只剩最后一次机会,那就是消息重定向。这个时候runtime会将未知消息的所有细节都封装为NSInvocation对象,然后调用下述方法:

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

    forwardInvocation:消息给这个问题提供了一个更特别的,动态的解决方案:当一个对象由于没有相应的方法实现而无法响应某消息时,运行时系统将通过forwardInvocation:消息通知该对象。每个对象都从 NSObject类中继承了forwardInvocation:方法。然而,NSObject 中的方法实现只是简单地调用了 doesNotRecognizeSelector:。通过实现您自己的forwardInvocation:方法,您可以在该方法实现中将消息转发给其它对象。

    要转发消息给其他对象时,forwardInvocation:方法所必须做的有:

    • 决定将消息转发给谁
    • 并且,将消息和原来的参数一块转发出去

    注意:forward意思是“转寄”,forwardingTargetForSelector:和forwardInvocation:都是把消息转发给一个新的接收对象。

    这里消息可以通过invokeWithTarget:方法来转发:

    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        NSLog(@"forwardInvocation");
        if ([RuntimeMethodHelper instancesRespondToSelector:anInvocation.selector]) {
            [anInvocation invokeWithTarget:_helper];
        }
    }
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
        if (!signature) {
            if ([RuntimeMethodHelper instancesRespondToSelector:aSelector]) {
                signature = [RuntimeMethodHelper instanceMethodSignatureForSelector:aSelector];
            }
        }
        return signature;
    }
    

    运行结果:

    2018-06-12 17:21:22.255362+0800 getIP[14454:801341] forwardInvocation
    2018-06-12 17:21:22.255588+0800 getIP[14454:801341] <RuntimeMethodHelper: 0x604000016fd0>, 0x10c80f8e9
    

    转发消息后的返回值将返回给原来的消息发送者。你可以返回任何类型的返回值,包括id,结构体,浮点数等。

    总结

    至此,我们了解到了runtime是如何通过objc_msgSend在运行时把方法和方法实现进行动态绑定的;也了解到如果沿继承树找不到IMP,如何进行动态方法解析和消息转发的。

    相关文章

      网友评论

          本文标题:runtime - 理解OC的消息和消息转发机制

          本文链接:https://www.haomeiwen.com/subject/vtmebctx.html