谈一谈OC运行时及消息转发

作者: Levi_ | 来源:发表于2015-07-16 18:30 被阅读2111次

    运行时

    网上对运行时机制有很多笼统的说法,但相信还是会有很多人并不完全理解运行时的机制。那么什么是运行时呢?
    先来看一种写法:

    @interface MyClass : NSObject 
    {
        @public
            NSString *_myName;
        @private
            NSString *_myID;
    }
    

    OC中是支持public和private关键字的,类似于java或C#,但是我们在编写OC代码的时候却很少这么做。因为使用这种写法,对象布局在编译器就已经固定了。只要碰到访问_myName变量的代码,编译器就把其替换为偏移量(offset),这个偏移量是硬编码,表示该变量距离存放对象的内存区域的起始地址有多远。但是如果在运行过程中,又新增了一个实例变量,硬编码于其中的变量就会读到错误的值,例如我们假设每个变量的指针都是4个字节,新增一个变量myGender:

    @interface MyClass : NSObject 
    {
        @public
            NSString *_MyGender;(offset为0)
            NSString *_myName;(offset为+4)
        @private
            NSString *_myID;(offset为+8)
    }
    

    如果offset采用硬编码,原类中offset为0是应该访问到_myName现在却访问到_myGender。
    OC的做法是,把实例变量当做一种存储offset所用的特殊变量,交由类对象保管。offset会在运行期查找,如果类的定义变了,那么offset也会相应改变,这样的话,无论何时访问实例变量,总能访问到正确的偏移位置。
    这就是我们所说的运行时机制。想更深入的了解运行时机制可以看看这篇文章:
    http://quotation.github.io/objc/2015/05/21/objc-runtime-ivar-access.html
    这篇文章同时解释了为什么OC无法动态添加成员变量。

    消息转发

    再来说一说消息转发,消息转发是运行时机制的一大特点。在了解消息转发之前先来了解一个OC的底层函数:

    void objc_msgSend(id self, SEL cmd, ...)
    

    在OC中,所有的方法最终都会转换为普通的C语言函数,例如一个对象object的方法:

    - (id)doSomething:(id)parameter
    {
        //doSomething
    }
    

    给object发送消息:

    id returnValue = [object doSomething:patameter];
    

    object是接受者,doSomething叫做选择子,选择子与参数合起来叫做消息,编译器看到此消息后,会将其转换为一条标准的C语言函数,就是最开始的那个函数,它是消息传递机制中的核心函数,上述消息会转换为:

    id returnValue = objc_msgSend(object,@selector(doSomething:),parameter);
    

    objc_msgSend函数会根据接受者与选择子的类型来调用适当的方法。为了完成此操作,该方法需要在接受者所属的类中搜寻方法列表,如果找到了名称相符的方法,就跳转至其实现代码,如果找不到,就沿着继承体系向上查找。所过最终依然没有找到相符的方法,就会执行消息转发
    一个完整的消息转发过程会经历三个阶段:

    • 动态方法解析(resolveInstanceMethod或resolveClassMethod)
    • 备选接收者(forwardingTargetForSelecor)
    • 完整消息转发(forwardInvocation)

    </br>

    1. 动态方法解析

    在消息转发开始时,本类有机会新增一个处理选择子的方法,如果选择子是实例方法会调用:

    + (BOOL)resolveInstanceMethod : (SEL)selector
    

    如果是类方法,则会调用:

    + (BOOL)resolveClassMethod : (SEL)selector
    

    例如,用此方案来实现@dynamic属性:

    id autoDictionaryGetter(id self, SEL _cmd);
    id autoDictionarySetter(id self, SEL _cmd, id value);
     
    + (BOOL)resolveInstanceMethod:(SEL)selector
    {
        NSString *selectorString = NSStringFromSelector(selector);
        if (/*选择子是个@dynamic属性*/)
            if ([selectorString hasPrefix:@"set"]) {
                class_addMethod(self, selector, (IMP)autoDictionarySetter, "v@:@");
            } else {
                class_addMethod(self, selector, (IMP)autoDictionaryGetter, "@@:");
            }
            return YES;
        }
        return [super resolveInstanceMethod:selector];
    }
    

    如果前缀为set,就是set方法,否则是get方法。不管哪种情况,都会把处理该选择子的方法动态的加到类里面,以最在开始处理消息转发。例子中用到了IMP指针,想了解IMP指针可以看看这篇博客:
    http://www.jianshu.com/p/425a39d43d16?utm_campaign=maleskine&utm_content=note&utm_medium=writer_share&utm_source=weibo

    2. 备选接收者

    如果在动态方法解析时没有处理消息转发,那么还有第二次机会来处理未知的选择子,在这一步中系统会询问:有没有其他接收者来处理这条消息?该步骤对应的处理方法如下:

    - (id)forwardingTargetForSelector:(SEL)selector
    

    但是我们无法操作经由这一步所转发的消息,如果想在发送给备选接受者之前先修改消息内容,就得通过完整的消息转发机制来做了。

    3. 完整的消息转发

    首先,创建NSInvacation对象,把与尚未处理的那条消息有关的全部细节都封装在其中,此对象包括选择子,target及参数。在触发NSInvocation对象时,消息转发系统将亲自出马,把消息指派给目标对象。
    此过程会调用:

    - (void)forwardInvocation:(NSInvocation *)invocation
    

    这个方法可以实现的很简单:只要改变调用target,使消息在新targer上得以调用即可。然而这样的实现就与备选接受者的实现方法等效了。一般的做法是:在触发消息前,先以某种方式改变消息内容,比如追加另一个参数,或更改选择子等等。
    实现此方法时,若发现调用操作不应由本类处理,则需调用超类的同名方法。这样的话,继承体系中的每个类都有机会处理此调用请求,直至NSObject。如果最终此消息未得到处理,则会调用NSObject的doesNotRecognizeSelector:,以抛出异常。
    完整的消息转发用到NSInvocation对象,想了解NSInvocation可以看一看这篇博文:
    http://mp.weixin.qq.com/s?__biz=MjM5NTIyNTUyMQ==&mid=208927760&idx=1&sn=30b9caecba709553e463d719668454ae&scene=2&from=timeline&isappinstalled=0#rd

    4. 完整的消息转发流程图

    另外需要注意的是,消息转发过程中,步骤越往后,处理消息的代价就越大,最好能在第一步就处理完,这样的话,运行期系统可以将此方法缓存。如果这个类的实例还会再接收到同名选择子,那么根本无须再次启动消息转发流程。
    另外一篇介绍运行时机制的博文:
    http://www.cocoachina.com/ios/20150715/12540.html

    相关文章

      网友评论

      • 蚂蚁牙齿不黑:oc 是可以动态添加成员变量的
        Levi_:你指的是objc_setAssociatedObject吧,成员变量本质是ivar,可以动态添加getter和setter,但是没办法动态添加ivar。
      • 火星的蝈蝈:写的很好不过我还没达到能看懂的层次,等我再牛逼点再来拜读!

      本文标题:谈一谈OC运行时及消息转发

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