RunTime

作者: 小凡凡520 | 来源:发表于2019-11-18 15:48 被阅读0次
    一、引言

    总所周知,高级语言想要成为可执行文件需要 先编译为汇编语言 -> 再汇编为机器语言,机器语言也就是计算机能够识别的唯一语言,但是OC并不能直接编译为汇编语言,而是需要先转写为纯C语言再进行编译和汇编的操作。

    从OC到C语言的过渡就是由RunTime来实现的,然而OC是进行面向对象的开发,而C语言更多的是面向过程开发,这就需要将面向对象的类转变为面向过程的结构体。

    二、什么是RunTime

    RunTime简称运行时,就是系统在运行的时候的一些机制,其中最主要的是消息机制。

    Objective-C语言作为一门动态语言,就意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发等。这种动态语言的优势在于:我们的代码更具有灵活性。而这个运行时系统就是Objc RunTime。

    • Objc RunTime
      其实是一个RunTime库,它基本上是使用 C 和 汇编 语言写的,具有面向对象的能力,是Objective-C面向对象和动态机制的基石。
    • 对于C语言
      函数的调用在编译的时候会决定调用哪个函数,在编码阶段,如果C语言调用未实现的函数就会报错
    • 对于OC语言
      是属于动态调用的,在编译时并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应函数来调用。
      • 在编译阶段,OC可以调用任何函数,即使用这个函数并未实现,只要声明过就不会报错
      • 当调用A对象上的某个方法B时,如果A对象并没有实现这个方法,可以通过“ 消息转发 ”来解决,只要对B方法进行声明,则在编译时不会报错
    三、消息相关常用内容
    • SEL
      SEL又叫选择器,是表示一个方法的selector的指针,其定义如下:
      typedef struct objc_selector *SEL;
      
    • IMP
      IMP实际上是一个函数指针,指向方法实现的首地址。其定义如下:
       id (*IMP)(id, SEL, ...)
      
      这个函数使用当前CPU架构实现的标准的C调用约定
      • 第一个参数是指向 self 的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针)
      • 第二个参数是方法选择器 (selector)
      • 接下来是方法的实际参数列表。
    • Method
      Method用于表示类定义中的方法,其定义如下:

      typedef struct objc_method *Method;
      struct objc_method {
          SEL method_name                 OBJC2_UNAVAILABLE;// 方法名
          char *method_types              OBJC2_UNAVAILABLE;
          IMP method_imp                 OBJC2_UNAVAILABLE;// 方法实现
      }
      
    • Method List
      每一个类都有一个方法列表Method List,它保存着类里面所有的方法,根据SEL传入的方法编号找到对应的方法,然后找到方法的实现,最后在方法的实现里面实现对应的具体操作。

      //方法列表
      struct objc_method_list {
          struct objc_method_list *obsolete           OBJC2_UNAVAILABLE;
          int method_count                            OBJC2_UNAVAILABLE;
      #ifdef __LP64__
          int space                                   OBJC2_UNAVAILABLE;
      #endif
          /* variable length structure */
          struct objc_method method_list[1]           OBJC2_UNAVAILABLE;
      }
      
    • Class
      Objective-C 类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针。它的定义如下:

      typedef struct objc_class *Class;
      

      查看objc/runtime.h中objc_class结构体的定义如下:

      struct objc_class {
          Class _Nonnull isa  OBJC_ISA_AVAILABILITY; //isa指针指向Meta Class
      
      #if !__OBJC2__
          Class _Nullable super_class                              OBJC2_UNAVAILABLE; // 父类
          const char * _Nonnull name                               OBJC2_UNAVAILABLE; // 类名
          long version                                             OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
          long info                                                OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识
          long instance_size                                       OBJC2_UNAVAILABLE; // 该类的实例变量大小
          struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE; // 该类的成员变量链表
          struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE; // 方法定义的链表
          struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE; // 方法缓存
          struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE; // 协议链表
      #endif
      
      } OBJC2_UNAVAILABLE;
      /* Use `Class` instead of `struct objc_class *` */
      

      在这个定义中,着重注意下面几个字段:

      1、isa
          需要注意的是在 Objective-C 中,所有的类自身也是一个对象,这个对象的Class里面也有一个isa指针,它指向metaClass(元类)。
          当我们向一个对象发送消息时,RunTime 会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。
      2、super_class
          指向该类的父类,如果该类已经是最顶层的根类(如NSObject或NSProxy),则super_class为NULL。
      3、cache
          用于缓存最近使用的方法。一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差。这时,cache就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法。这样,对于那些经常用到的方法的调用,但提高了调用的效率。
      4、version
          我们可以使用这个字段来提供类的版本信息。这对于对象的序列化非常有用,它可是让我们识别出不同类定义版本中实例变量布局的改变。
      
    四、消息传递 - 动态查找

    消息机制是运行时里面最重要的机制,OC是动态语言,本质都是发送消息,每个方法在运行时会被动态转化为消息发送,即:objc_msgSend(receiver, selector)

    • OC代码 - 实例方法 调用底层的实现:
    BackView *backView = [[BackView alloc] init];
    [backView changeBgColor];
    
    //编译时底层转化
    //objc对象的isa指针指向他的类对象,从而可以找到对象上的方法
    //SEL:方法编号,根据方法编号就可以找到对应方法的实现。
    [backView performSelector:@selector(changeBgColor)];
    
    //performSelector本质即为运行时,发送消息,谁做事情就调用谁 
    objc_msgSend(backView, @selector(changeBgColor));
    // 带参数
    objc_msgSend(backView, @selector(changeBgColor:),[UIColor RedColor]);
    
    • OC代码 - 类方法 调用底层的实现
    //本质是将类名转化成类对象,初始化方法其实是创建类对象。
    [BackView changeBgColor];
    //BackView 只是表示一个类名,调用方法其实是用的类对象去调用的。(类对象既然称为对象,那它也是一个实例。类对象中也有一个isa指针指向它的元类(meta class),即类对象是元类的实例。元类内部存放的是类方法列表,根元类的isa指针指向自己,superclass指针指向NSObject类。)
    
    //编译时底层转化
    //RunTime 调用类方法同样,类方法也是类对象去调用,所以需要获取类对象,然后使用类对象去调用方法
    Class backViewClass = [BackView class];
    [backViewClass performSelector:@selector(changeBgColor)];
    //performSelector本质即为运行时,发送消息,谁做事情就调用谁 
    
    //类对象发送消息
    objc_msgSend(backViewClass, @selector(changeBgColor));
    // 带参数
    objc_msgSend(backViewClass, @selector(changeBgColor:),[UIColor RedColor]);
    

    一个对象的方法像这样[obj changeBgColor],编译器转成消息发送objc_msgSend(obj, changeBgColor),Runtime 时执行的流程是这样的:

    1、实例对象调用方法后,底层调用[objc performSelector:@selector(SEL)];方法,编译器将代码转化为objc_msgSend(receiver, selector)。
    2、在objc_msgSend函数中:
      a)首先通过objc的isa指针找到objc对应的class类的结构体
      b)在class中,先去cache中通过SEL查找对应函数的 method,如果找到则通过 method中的函数指针跳转到对应的函数中去执行。
      c)如果在cacha中未找到,再去methodList中查找,如果能找到,则将method加入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。
      d)如果在methodlist中未找到,则通过objc_msgSend结构体中的指向父类的指针找到其父类,并在superClass的分发表中去查找方法的selector,如果能找到,则将method加入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。
      e)如果在methodlist中未找到,则通过objc_msgSend结构体中的指向父类的指针找到其父类,并在superClass的分发表中去查找方法的selector,如果能找到,则将method加入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。
      f)如果最后依旧没有定位到selector,则会走消息转发流程。
    
    五、消息转发

    默认情况下,如果是以[object message]的方式调用方法,如果object无法响应message消息时,编译器会报错。但如果是以perform...的形式来调用,则需要等到运行时才能确定object是否能接收message消息。如果不能,则程序崩溃并抛出异常,通过控制台,我们可以看到以下异常信:

    - xxxx : unrecognized selector sent to instance xxxx
    

    消息转发机制的三个步骤:动态方法解析 / 备援接收者 / 消息重定向


    1573112231932779.png
    • 动态方法解析
      对象在接收到未知的消息时,首先会调用所属类的实例方法 +resolveInstanceMethod: 或者类方法 +resolveClassMethod: 。

      在这个方法中,我们有机会为该未知消息新增一个”处理方法”。不过使用该方法的前提是我们已经实现了该”处理方法”,只需要在运行时通过class_addMethod函数动态添加到类里面就可以了。如下代码所示:

      void functionForMethod1(id self, SEL _cmd) {
         NSLog(@"%@, %p", self, _cmd);
      }
      
      + (BOOL)resolveInstanceMethod:(SEL)sel {
          NSString *selectorString = NSStringFromSelector(sel);
          if ([selectorString isEqualToString:@"method1"]) {
              class_addMethod(self.class, @selector(method1), (IMP)functionForMethod1, "@:");
          }
          return [super resolveInstanceMethod:sel];
      }
      

      在objc运行时会调用 +resolveInstanceMethod: 或者 +resolveClassMethod: ,让你有机会提供一个函数的实现。如果你添加了函数,那运行时系统就会重新启动一次消息发送的过程,否则 ,运行时就会移到下一步,消息转发(Message Forwarding)。

    • 备援接收者
      如果在上一步无法处理消息,则 Runtime 会继续调以下方法:

      - (id)forwardingTargetForSelector:(SEL)aSelector
      

      如果一个对象实现了这个方法,并返回一个非nil的结果,则这个对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是self自身,否则就是出现无限循环。整个消息发送的过程会被重启,并且发送的对象会变成你返回的那个对象。当然,如果我们没有指定相应的对象来处理 aSelector,那么应该调用父类的实现来返回结果。

    • 消息重定向
      如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制进行消息重定向了。这个时候 RunTime 会将未知消息的所有细节都封装为 NSInvocation 对象,然后调用下述方法:

      - (void)forwardInvocation:(NSInvocation *)anInvocation
      

      运行时系统会在这一步给消息接收者最后一次机会将消息转发给其它对象。对象会创建一个表示消息的NSInvocation对象,把 与尚未处理的消息有关的全部细节都封装在anInvocation中,包括selector,目标(target)和参数。我们可以在forwardInvocation方法中选择将消息转发给其它对象。

    相关文章

      网友评论

          本文标题:RunTime

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