美文网首页
Runtime介绍

Runtime介绍

作者: 行走的栀子花 | 来源:发表于2016-12-19 14:46 被阅读69次

    以下文章很多的参考了 iosswift的翻译的文章。

    这是我在简书上写的第三篇文章,喜欢的朋友请点个赞,给我加加油~
    iOS的runtime是一个大话题,对这个话题我也不大了解,始终相信查看apple的doc对深入理解iOS很有帮助,不积跬步无以至千里,下面正式开始。

    运行时介绍

    OC语言设计成尽可能多的将一些决定从编译链接时刻延迟到运行时刻,它尽可能多的动态处理事情。这意味着OC语言不仅仅需要一个编译器,它还需要一个运行时系统来执行编译好的代码。运行时充当OC语言的一种操作系统,使得OC语言能正常工作。

    本文着眼于NSObject类和OC程序是怎样和运行时系统交互的,特别会讨论在运行时OC动态加载新的类的范例,以及传递消息给其他对象。这篇文章还提供了当程序在运行时候,你是怎么获取对象的信息的。

    你应该阅读这篇文章,以此来理解OC的运行时系统是怎么工作的,以及我们怎么利用运行时。在编写cocoa应用之前,你还是有必要去了解和理解这份材料的。

    这篇文档的结构如下:
    1. 运行时的版本以及平台
    2. 与运行时的交互
    3. 消息传递
    4. 动态方法解析
    5. 消息转发
    6. 类型编码
    7. 属性声明
    参阅

    Objective-C Runtime Reference(运行时接口) 描述了OC运行时库的数据结构和方法。你的程序也可以使用这些接口和OC的运行时系统进行交互。举个例子:我们可以使用运行时接口 来添加类、添加方法或者获取所有已加载类的类定义列表
    Programming with Objective-C 介绍了OC语言
    Objective-C Release Notes 介绍了最近发布的OS X系统中,OC运行时的一些改变.

    下面开始介绍runtime结构中的第一部分:运行时的版本以及版本对应的平台

    1.运行时的版本以及平台(Runtime Versions and Platforms)

    在不同的平台上有不同的OC运行时版本

    历史版本和当前版本

    现在有两个OC运行时版本:当前版本和历史版本. 当前版本是由OC2.0引进的,包含了一系列的新特性。历史版本的运行时接口在Objective-C 1 Runtime Reference中被介绍。当前版本的运行时接口在Objective-C Runtime Reference中被介绍。
    在当前运行时版本中,最重要的新特性是实例变量的稳固性:

    • 历史版本的运行时,如果你修改类中的一个变量的显示,你必须要重新编译继承这个类的子类们。
    • 当前版本的运行时,如果你修改类中的一个变量的显示,你不需要重新编译继承这个类的子类们(子类也可以有这个类修改后的变量);

    另外,当前运行时支持对申明的属性进行实例变量的合成。

    平台

    iPhone应用以及在 v10.5之后的64位OS X系统上的应用使用的是当前版本的运行时。其他在32位操作系统的OS X上的应用使用的是老版本的运行时

    下面开始介绍runtime结构中的第二部分:程序与运行时的交互

    2.与运行时的交互

    OC程序与运行时系统的交互有三种等级:通过OC源码,通过调用定义在Foundation框架中的NSObject类里的方法,只掉调用运行时方法。

    OC源代码

    在大多数情况加,运行时自动运行在后台,你只能通过编写或者运行OC源码才能使用它。

    当你编译一段包含OC类和方法的代码时,编译器会创建数据结构以及函数调用来实现OC语言的动态特性。数据结构会从类和类别的定义中获取信息,从协议的声明中获取信息。这些信息包括 在 Defining a Class and Protocols in The Objective-C Programming Language 这边文章中讨论的类和类别对象,也包括方法选择器,实例变量模板和从源码中获取的其他信息。

    NSObject方法

    大多数Cocoa的对象都是NSObject类的子类,故大多数的对象都继承了NSObject定义的方法(NSProxy类是一个例外,更多信息参考Message Forwarding)。因此NSObject的方法被每一个实例变量和类对象所拥有。然而在少数情况下,NSObject类值定义了一个模板,告诉这件事情应该怎么做,并没有提供具体的代码。

    例如:NSObject类定义了一个description实例方法来返回一个字符串,该字符串藐视了该类的内容。这个实例方法在debugging的时候非常有用:GDB的print-object命令就是打印descriotion方法返回的字符串内容。NSObject类中该方法的实现并不知道该类包含了什么,所以该方法返回一个包含对象类型名称和对象地址的字符串。NSObject的子类可以实现这个方法,从而返回更多更详细的信息。例如:Foundation框架中的NSArray返回一个其内部所有对象的描述列表。

    一些NSObject方法会向运行时请求信息,这些方法允许对象进行自我判断。例如class方法获取对象确定自己的class,isKindofClass: isMemberofClass测试对象类在继承中的位置,respondsToSelector:判断对象是否可以接受一个指定的消息,conformsToProtocol:判断对象类是否实现了指定协议的方法, methodForSelector:提供一个方法的实现地址。这些方法给对象提供了检查自己的能力。

    运行时函数

    运行时是一个动态共享库,包含一个公共的接口,这个接口是由一系列的函数以及数据结构组成,这些接口位于/usr/include/objc中。许多函数方法允许你使用简单的C语言去重写功能,正像编译环节对OC代码做的工作一样。其他一些函数方法组成NSObject方法的基础功能。这些方法允许开发runtime的新接口,以及制造一些工具来扩展开发环境。这些方法可能在编写OC代码的时候不需要是哟空,但是有些函数方法会在编写OC程序的时候很有用,所有的函数在Objective-C Runtime Reference中。

    这一节主要说程序员与runtime打交道的三种方式,第一种是编写OC源码的时候会用到,我们一般的程序员是用不到了,只有编写苹果系统的那些员工才可以用到吧,他们是造轮子的人。第二种我们通过调用NSObject类的一些方法可以间接的调用runtime,比如我创建了一个对象,该对象调用了一个方法,这个方法就用到了运行时,潜在的用到了,(虽然我们表面上看不出来)。第三种直接调用运行时的一些函数。

    下面开始介绍runtime结构中的第三部分:消息的传递

    3.消息的传递(Messaging)

    这篇文章描述了信息表达式是怎么被转换成调用objc_msgSend函数的,以及程序是怎么通过函数名查询函数的实现方法的。接着会告诉你怎么利用objc_msgSend,以及怎么绕开动态绑定。

    objc_msgSend 函数

    在OC中,消息是不会接通到方法的具体实现部分,直到执行这段代码的时候。编译器将这段消息表达式:

     [receiver message] ```
    转换成objc_msgSend的函数的调用,_msgSend函数会获取消息执行者,以及方法的申明作为他的参数,如下:
    

    objc_msgSend(receiver, selector)```

    参数也会被传进这个函数里:

    objc_msgSend(receiver, selector, arg1, arg2, ...)```
    
    >在objc_msgSend函数里,它会做完动态绑定所需要的一切事情,执行过程如下:
    - 首先objc_msgSend函数会在内部获取selector的实现代码部分,由于同一个方法可以在不同的类中申明,所以selector的实现代码部分是通过receiver的类型决定的。
    - 然后objc_msgSend函数会在内部执行selector的实现代码部分,将receiver的对象以及一些参数传递进selector的实现代码中
    - 最后,objc_msgSend会将selector实现部分的返回结果作为自己的返回结果返回。
    
    > 说明:编译器会产生消息传递功能的调用函数(即objc_msgSend),我们不能在代码中直接调用它。
    
    >消息传递的关键在于编译器为每一个class和object创建的结构体。每一个class的结构体中包含如下2个必须的元素:
    - 一个指向父类的指针
    - 一个class dispatch表(调度表)。这个列表包含该类的方法申明以及方法对应实现代码的地址。例如setOrigin::方法的申明关联了setOrigin::方法的实现地址,display方法的申明关联了display方法的实现地址。
    
    > 当一个新对象被创建时,这个对象的内存也被开辟出来了,并且他的实例变量也会被初始化完毕。在对象内部众多的变量之间,第一个变量就是一个指向该对象对应class的指针。这个指针叫做isa。使得该对象能访问到其对应的class,通过该class又可以获取继承的class。
    > 说明: 虽然严格来说isa不是OC语言中的一部分,但是isa指针确是一个对象在运行时工作所必须要的条件。你极少需要创建自己的根类,继承自NSObject或者NSProxy的对象会自动拥有一个isa变量。
    
    >类对象包含的元素,以及对象结构体表示如下图:
    ![3142B2CB-D27C-4909-ACA6-16F2AA328CD9.png](https://img.haomeiwen.com/i2317644/60cbafdc7e7e7ccf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    > 当消息发送给一个对象时,消息传递函数(objc_msgSend)会根据对象的isa指针获取类对象的结构体,进而获取类对象内部的方法调度列表,如果找不到需要的方法,objc_msgSend内部会根据类对象的isa指针获取其父类对象的地址,查找方法调度列表,直到继承树的最顶部。一旦找到这个方法,objc_msgSend会调用这个方法申明对应的实现部分,并将receiver对象传递他。
    
    > 这就是在runtime时,程序根据不同的接收者执行不同的方法的原理。以面向对象的编程术语来说,这叫给消息动态绑定方法。
    
    > 为了加快消息的执行,运行时系统缓存了已经被使用过的方法以及这些方法对应的执行地址。每一个class都有一个独立的缓存区域,并且这个缓冲区还可以包含继承类的方法,这些继承类的方法存放的方式就跟该类本身的方法一样。在搜索dispatch列表之前,objc_msgSend方法首先会搜索receiver对应类对象的缓存区(因为理论上说被用过一次的方法极有可能会被再次使用到)。如果需要调用的方法在缓存区,那么消息的处理仅仅比直接调用函数实现代码慢一点点。一旦一个程序运行了足够长的时间,那么几乎所有发送的消息方法都能在缓存中被发现。程序运行的时候,缓存区也会动态的容纳新的消息方法。
    
    > ######使用隐藏的参数
    > 当objc_msgSend方法查找到方法对应的实现代码时,它开始执行这段代码并且将消息中所有的参数传递给这段代码,传递的参数中包含两个隐藏的参数:
    - 消息的接受者
    - 消息执行的方法
    
    > 这些参数提供了执行方法函数所需要的两部分明确的信息。之所以说这两个参数是隐藏的,是因为在申明这个方法的时候,并没有申明这两个参数作为参数。这两个参数是在代码编译完成之后,有苹果系统放入方法的实现部分了。
    > 虽然这两个参数没有被明确的申明出来,但是源码还是可以关联到他们(就像代码的实现部分可以关联到消息接受者的变量一样)。一个方法通过self关联到receiver,通过_cmd关联到selector。在下面的例子中,_cmd关联到strange的方法,self关联到一条strange消息的接收着。
    
    
    • strange
      {
      id target = getTheReceiver();
      SEL method = getTheMethod();

      if ( target == self || method == _cmd )
      return nil;
      return [target performSelector:method];
      }

    > self是这两个隐藏参数中最有用的一个。它使得receiver的成员变量可以在方法中被使用。
    
    > ######获取一个方法的地址(即方法实现的地址)
    > 唯一避免动态绑定的方法是获取方法的地址,然后直接调用他就像他是一个函数一样。这种直接调用的方式适合在一个方法被执行很多次,并且你希望减少每次搜索方法造成的时间浪费的情况下使用。
    
    > 通过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);

    >传进方法中的前两个参数是receiver和方法选择器。这两个参数在方法中是隐藏的,但是如果像调用函数一样的调用方法的实现必须要提供这两个参数。
    就像上文提到的,在编译环节,苹果已经帮我们处理了这两个默认的参数,所以如果我们手动的调用函数,需要手动的传递这两个参数到函数中。
    
    >使用 methodForSelector: 函数避免动态绑定会减少消息传递过程中的大部分时间,但是这只有在指定的消息在被重复很多次的情况下才会有意义,就像上面提到的for循环。
    > methodForSelector: 是有cocoa的runtime系统提供的功能,而不是Objective_C语言的特性。
    
    接下来说第三部分:动态添加方法
    > ###3.动态添加方法
    > 这章介绍你怎么动态的添加一个方法的实现
    
    > #####添加动态方法的实现
    >有些情况下你可能想要动态的给一个方法提供实现函数。例如,Objective-c申明的属性特性中就包含@dynamic指令:
    

    @dynamic propertyName;```

    这个指令告诉编译器,与这个属性相关联的方法会被动态的提供。

    我们可以通过 resolveInstanceMethod: 和 resolveClassMethod: 方法,动态的为一个类添加实例方法或者类方法的实现。

    一个Objective-C方法实际上是一个至少包含两个参数的C语言函数。这两个参数是self和_cmd。 你可以通过class_addMethod方法来将一个C语言函数作为一个方法添加到类中。因此,添加下面这段函数作为类的一个方法实现部分:

    void dynamicMethodIMP(id self, SEL _cmd) {
        // implementation ....
    }
    

    你可以通过resolveInstanceMethod:方法将这个C函数作为一个方法动态的添加到类中(假设这个函数对应的方法名称叫resolveThisMethodDynamically):

    @implementation MyClass
    + (BOOL)resolveInstanceMethod:(SEL)aSEL
    {
        if (aSEL == @selector(resolveThisMethodDynamically)) {
              class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
              return YES;
        }
        return [super resolveInstanceMethod:aSEL];
    }
    @end```
    
    > 通常消息传递和动态方法实现是不相干的。在执行转发消息机制之前,消息的接收者对应的类有机会动态的实现这个方法。如果类的resolveInstanceMethod:和instancesRespondToSelector: 方法被执行的话,那么我们就有机会动态的为这个方法提供一个IMP(方法实现代码)。如果你在类中实现了 resolveInstanceMethod:方法,但是想要指定的某个方法依然走消息转发机制,那么返回NO就可以了。
    
    > #####动态加载
    > Objective-c程序可以在运行时加载和连接一些类和类别。这些被添加到程序中的新类和类别的功能和程序运行起来之前加载的类和类别一模一样。
    
    > 动态加载可以做很多不同的事情。例如系统设置中的很多应用都是动态加载的。
    
    > 在Cocoa环境中,动态加载一般被用来给应用做自定义化。你的代码可以在运行时加载别人写的代码,就像Interface Builder载入用户自定义的模块,OS X系统偏好设置载入自定义设置模块一样。可加载模块只需要得到许可,就可以扩展应用的功能,而不需要你自己预先定义或者实现它。你提供框架,其他人提供实现的代码。
    
    > 尽管运行时提供了一个动态加载模块的函数(在 objc/objc-load.h文件中的objc_loadModules函数),但是Cocoa的NSBundle类提供了一个更方便的动态加载的接口——一个面向对象的并且和相关服务相结合的方法。请参考Foundation框架里的NSBundle类说明文档。对于Mach-O files的信息请参开OS X ABI Mach-O File Format Reference。
    
    总结一下:动态添加方法是指,当某个方法在编译之前没实现的话,可以在运行时刻添加该方法的实现函数。一个消息发送给了一个对象,如果该对象在该类中没有查找到这个方法对应的实现函数,如果你在该类中定义了+ (BOOL)resolveInstanceMethod:(SEL)sel方法,那么该类会在把消息往父类转发之前,先调用这个方法,查找有没有给这个方法添加实现函数,如果有那么就跳转到具体的函数里去执行,如果没有才进行消息的转发。
    
    不仅可以进行动态添加方法的实现函数,还可以动态添加类,类别,这些与正常编写的类和类别的功能没任何差别,好处就是可以把实现部分交给其他程序员来实现,而我只负责定义有这么一个功能即可。
    
    > ###4.消息转发
    > 给一个对象发送一个它没有处理的消息会出错。在报错之前,运行时系统会给这个对象一个机会去处理这个消息。
    
    > ######消息转发
    > 如果你给一个对象发送一条它无法处理的消息,那么在报错之前,运行时会给这个对象发送一条forwardInvocation:消息,这个消息包含一个NSInvocation类型的参数。NSInvocation对象包含了原始的消息以及原始消息中的所有参数。
    
    > 你可以通过实现forwardInvocation:方法来给一条消息作出默认的响应,以此来避免报错。就像这个方法名字暗示的那样,forwardInvocation:通常用来将这条消息传递给其他对象。
    
    > 为了了解传递的范围和目的,设想有如下情景:首先,假设你需要设计一个可以响应方法名叫negotiate的消息的对象,并且这个对象可以响应其他对象对negotiate消息的响应。当然你可以通过在negotiate方法内部,将negotiate消息传递给其他对象来实现这个功能。
    
    > 更深入一层,假如你想要你的一个对象响应一个在其他类中实现的negotiate方法,一个方法是让你的class继承实现这个negotiate方法的类,但是如果你的class和实现这个negotiate方法的类在不同的继承分支中,就不能用这种方法了。
    
    > 虽然你的class不能继承实现negotiate方法的类,但是你仍然可以通过将这个消息传递给另外一个对象来达到这个效果,将其他对象执行的方法作为自己的返回结果。代码如下:
    
    • (id)negotiate
      {
      if ( [someOtherObject respondsTo:@selector(negotiate)] )
      return [someOtherObject negotiate];
      return self;
      }
    > 这样做有一写不好的地方,特别是当有很多条消息你都想传递给另外一个对象的时候,这样你不得不为每一条需要借的方法实现一个对应的方法,此外,这种方式不能处理未知的消息。在你写这些代码的时候,你可能会想要传递所有的方法,这个方法的集合可能会在运行时改变,运行时可能会添加新的方法新的类。
    
    > forwardInvocation:方法提供了一个更好的解决办法,并且这个方法是动态的,而不是静态的。它的工作原理是这样的:当一个对象因为没有实现这个方法而不能响应这个消息的时候,运行时就会通过给这个对象发送一条 forwardInvocation: 消息来通知它。每一个对象都通过NSObject的继承从而获得了forwardInvocation:方法,然后NSObject的forwardInvocation:方法内部只是简单的执行了doesNotRecognizeSelector:方法。通过重写forwardInvocation:方法我们可以将这条消息转发给其他对象。
    
    > 为了传递消息,forwardInvocation:方法内需要做的事情有:
    - 决定消息应该传递给谁
    - 将原始的参数一并传递给需要传递的对象
    
    > 这条消息可以通过 invokeWithTarget: 方法传递出去。例子如下:
    
    • (void)forwardInvocation:(NSInvocation *)anInvocation
      {
      if ([someOtherObject respondsToSelector:
      [anInvocation selector]])
      [anInvocation invokeWithTarget:someOtherObject];
      else
      [super forwardInvocation:anInvocation];
      }
    > 消息转发后的函数处理的返回值会返回给原始的消息调用者。所有的数据类型都可以返回给调用者,包括id类型,结构体,double,float等。
    
    > forwardInvocation: 方法可以看做是未识别消息的转发中心,把未识别的消息转发给不同的接收者。或者可以将它作为一个中转站,将所有的消息都转发给同一个对象。他可以将消息转发给其他对象,也可以简单的忽略某些消息,从而让这些消息既不处理也不报错。forwardInvocation:方法让几种消息获得同一种响应,所以 forwardInvocation: 做什么取决于实现的人。总之,forwardInvocation: 方法为程序设计中,将消息转发给传递链中的一个对象提供了可能。
    
    > 说明:forwardInvocation: 方法只有在原始的接收者不能处理一个已经存在的方法的时候才会执行。比方说,你想要你的对象将 negotiate 消息传递给另外一个对象,那么的你的对象不能拥有 negotiate 方法,否则, forwardInvocation:方法永远不会被调用。
    
    > 关于转发和调用的更多信息,请参考Foundation 框架的 NSInvocation  接口说明。
    
    > ######消息转发和多重继承
    > 消息转发类似于继承,这种效果可以用来实现Objective-C程序的多继承功能。像下面的图,对象通过传递消息向另外一个类借用或者继承该方法的实现函数,从而响应这条消息。
    ![5EE9E921-E06C-45E4-90DF-813AFA2EA2A0.png](https://img.haomeiwen.com/i2317644/07c54aeee0b5d34a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    
    > 在上图中,一个Warrior类的实例对象将一个 negotiate 消息发送给 Diplomat类的实例对象。Warrior 将处理negotiate消息 就像 他是 Diplomat 对象一样。
    
    > 传递消息的对象因此“继承”了两个击沉体系分支——他自己的分支以及实际响应消息的对象的分支。在上例中,Warrior 类就像继承了Diplomat 类一样。
    
    > 消息传递提供了多继承的很多特性,然后这两者有一个重要的区别:多继承是将不同的行为封装到了一个对象中,他会是的对象更加庞大,更加复杂。另一方面,消息传递会把责任分配给不同的对象。虽然将任务分割成小的对象,但是又是以一种对于消息发送来说是特命的方式将这些小的对象关联起来了。
    
    > ######代理对象
    > 消息传递不仅仅像多重继承,它也使得可以让一个轻量级的对象,代替复杂的对象来进行开发。代理代表了其他对象并且像其他对象传递消息。
    
    > 在 The Objective-C Programming Language 书中的 “Remote Messaging” 中讨论的 proxy 就是这样一种代理。一个 proxy 对象负责管理将消息转发给远程接收者的细节,确保参数正确传递以及结果的正确返回,等等。但是他不会做更多的其他的事情了,代理类没有复制远程对象的功能,仅仅只是给了远程对象一个本地地址,让远程对象能够获取其他应用里的信息。
    
    

    相关文章

      网友评论

          本文标题:Runtime介绍

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