RunTime

作者: iChuck | 来源:发表于2018-03-19 10:04 被阅读16次

    runtime 是什么?

    • runtime 又叫做运行时,是一套底层的 C 语言API,其为 iOS 内部的核心之一,我们平时编写 oc代码,底层都是基于它来实现的。比如
    [receiver message]
    // 底层运行时会被编译器转化为:
    objc_msgSend(receiver, message);
    
    // 有参数的
    [receiver message:(id)arg...];
    
    objc_msgSend(receiver, seletor, arg1, arg2, ...);
    
    

    为什么需要 runtime

    • oc 是一门动态语言,它会将一些工作放在代码运行时才处理并非编译时。也就是说,有很多类和成员变量在我们编译的时候是不知道的,而在运行时,我们编写的代码才会被转换成完整的确定 的代码运行。
    • 因此,编译器是不够的,我们还需要一个运行时系统(runtime system)来处理编译后的代码。
    • runtime 基本是用 c 和汇编写成的,由此可见苹果为了动态系统的高效做出的努力。苹果的 GNU 各自维护一个开源的 runtime 版本,这两个版本之间都在努力保持一致。

    runtime 的作用

    • oc 在3个层面上与 runtime 系统进行交互:
    • 通过 oc 源码,只要需要 oc 代码,runtime 系统自动在幕后搞定一切,调用方法,编译器会将 oc 代码转化成运行时代码,在运行时确定数据结构和函数。
    • 通过 Foundation 框架的 NSObject 类定义方法。cocoa程序中绝大多数都是 NSObject 的子类,所有都继承了 NSObject 的行为。(NSProxy 类是个例外,它是一个抽象类)。
      • 一些情况下 NSObject 类仅仅定义了完成某件事情的模板,并没有提供所需要的代码。例如:- description 方法,该类方法返回类内容的字符串表示,该方法主要用来调试程序。NSObject 类并不知道子类的内容,所以它只是返回类的名字和对象的地址,NSObject 的子类可以重新实现。
      • 还有一些 NSObject 的方法可以通过 runtime 系统中获取信息,允许对象进行自我检查。例如:
        • -class 方法返回对象的类:
        • -isKindOfClass:和-IsMemberOfClass:方法检查对象是否存在指定的类的继承体系中(是否是其子类或者父类或者当前类的成员变量)
        • -respondsToSelector:检查对象是否响应指定的消息
        • -conformsToProtocol:检查对象是否实现了指定协议类的方法
        • -methodForSelector:返回指定方法实现的地址
    • 通过对 Runtime 库函数的直接调用
      • runtime 系统是具有公共接口的动态共享库。
      • 许多函数可以让你使用纯 C 代码实现 objc 同样的功能。除非是写一些 objc 与其他语言桥接或者底层的 debug 工作,你在写 objc 代码时一般不会用到这些 c 语言函数。

    runtime 的相关术语

    • SEL

      • 它是selector 在 objc 中的表示。selector 是方法选择器,其实作用和名字一样,日常生活中,我们通过人名辨别谁是谁,注意 objc 在相同的类中不会有命名相同的两个方法。selector 对方法进行包装,以便找到对应的方法实现。他的数据结构是:typedef struct objc_selector *SEL; 我们可以看出它是一个映射到方法 C 字符串,你可以通过 objc 编译器命令@selector()或者 runtime 系统的 sel_registerName 函数来获取一个 SEL 类型的方法选择器。
      • 注意:不同类中相同名字的方法对应的 selector 是相同的,由于变量类型不同,所以不会导致他们调用方法实现混乱。
    • id

      • id 是一个参数类型,他是指向某个类的实例指针。定义如下:
      typedef struct objc_object *id;
      struct objc_object {
          Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
      };
      
      • 以上定义,看到 objc_object 结构体包含一个 isa 指针,根据 isa 指针就可以找到对应所属的类。
      • 注意:isa 指针在代码运行时并不总指向实例对象所属的类型,所以不能依靠它来确定类型,要响确定类型还是需要用对象的 -class 方法。PS:KVO 的实现原理就是将被观察对象的 isa 指针指向一个中间类而不是真实类型。
    • Class

      • typedef struct objc_class *Class;
      • class 其实是指向 objc_class 的结构体的指针。objc_class 的数据结构如下
      
      struct objc_class {
      Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
      #if !__OBJC2__
      Class _Nullable super_class                              OBJC2_UNAVAILABLE;
      const char * _Nonnull name                               OBJC2_UNAVAILABLE;
      long version                                             OBJC2_UNAVAILABLE;
      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;、
      
      
      • 从 objc_class 可以看出,一个运行时类中关联了它的父类指针、类名、成员变量、方法、缓存以及附属协议。
      • 其中 objc_ivar_list 和 objc_method_list 分别是成员变量列表和方法列表:
      // objc_ivar_list 的实现
      
      struct objc_ivar_list {
          int ivar_count                                           OBJC2_UNAVAILABLE;
      #ifdef __LP64__
          int space                                                OBJC2_UNAVAILABLE;
      #endif
          /* variable length structure */
          struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;
      }   
        // objc_method_list的实现
      
      struct objc_method_list {
          struct objc_method_list * _Nullable 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;
      }    
      
      • 由此可见,我们可以动态修改 *methodList 的值来添加成员方法,这也是 category 实现的原理,同样解释了 Category 不能添加属性的原因
      • objc_ivar_list 结构体用来存储成员变量的列表,而 objc_ivar则是存储了单个成员变量的信息;同理,objc_method_list 结构体存储着方法数组的列表,而单个方法信息由 objc_method 结构体存储。
      • 值得注意的是,objc_class 中也有一个 isa 指针,这说明 objc 类本身也是一个对象。为了处理类和对象的关系,runtime 库创建一个叫做 Meta Class(元类)的东西,类对象所属的类叫做元类。meta Class 表述了对象本身所具备的元数据。
      • 我们所熟悉的类方法,就源自于 meta Class。我们可以理解为类方法就是类对象的实例方法。每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。
      • 当你发出一个类似[NSObject alloc](类方法)消息时,实际上,这个消息被发送给一个类对象(Class object),这个类对象丙戌是一个元类的实例,而这个元类同时也是一个根元类(root meta Class)的实例。所有元类的 isa 指针最终都指向根元类。
      • 所以当[NSObject alloc];这条消息发送给类对象的时候,运行时代码 objc_msgSend()会去元类中查找能够响应的方法实现,如果找到了,就会对这个类对象执行方法调用。
      • 最后 objc_class 中还有一个 objc_cahce,缓存。
    • method

      • method 代表类中某个方法的类型
      struct objc_method {
          SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
          char * _Nullable method_types                            OBJC2_UNAVAILABLE;
          IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
       }   
      
      • 方法类型是 SEL
      • 方法类型 method_types 是一个char 指针,存储方法的参数类型和返回值类型
      • method_imp 指向了方法实现,本质是一个函数指针
      • Ivar 是表示成员变量的类型。
      struct objc_ivar {
          char * _Nullable ivar_name                               OBJC2_UNAVAILABLE;
          char * _Nullable ivar_type                               OBJC2_UNAVAILABLE;
          int ivar_offset                                          OBJC2_UNAVAILABLE;
      #ifdef __LP64__
          int space                                                OBJC2_UNAVAILABLE;
      #endif
      }    
      
      • 其中 ivar_offset 是基地址便宜字节
    • IMP

      • IMP 在 objc.h 中定义的是
      typedef void (*IMP)(void /* id, SEL, ... */ ); 
      
      • 他是一个函数指针,这是由编译器生成的。当你发起一个 objc 消息之后,最终他会执行哪段代码,就是由这个函数指针制定的。而 IMP 这个函数指针就指向了这个方法的实现。
      • 如果得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法。
      • 你会发现 IMP 指向的方法与 objc_msgSend 函数类型相同,参数都包含了 id 和 SEL 类型。每个方法名都对应一个 SEL 类型的方法选择器,而每个实例中的 SEL 对应的方法实现肯定是唯一的,通过一组 id 和 SEL 参数就能确定唯一的方法实现地址。
      • 而一个确定方法也只有唯一一组 id 和 SEL 参数。
    • cache

      • 定义如下
      typedef struct objc_cache *Cache                             OBJC2_UNAVAILABLE;
      
      struct objc_cache {
          unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
          unsigned int occupied                                    OBJC2_UNAVAILABLE;
          Method _Nullable buckets[1]                              OBJC2_UNAVAILABLE;
      };
      
      • cache 为方法调用的性能进行了优化,每当实例对象接收一个消息时,它不会直接在 isa 指针指向的类的方法类别中遍历查找能够响应的方法,因为每次都要查找的效率太低了,而是优先在 cache 中找。
      • runtime 系统会吧调用到的方法 cache 中,如果一个方法被调用,那么他有可能今后还会被调用,下次查找的时候就会效率更高。就像计算机组成原理中 CPU 绕过主存先访问 cache 一样。
    • property

      typedef struct objc_property *objc_property_t;
      
      • 可以通过 class_copyPropertyList 和 protocol_copyPropertyList 方法获取类和协议中的属性
      OBJC_EXPORT objc_property_t _Nonnull * _Nullable
          class_copyPropertyList(Class _Nullable cls, unsigned int * _Nullable outCount)
          OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
      
        OBJC_EXPORT objc_property_t _Nonnull * _Nullable
          protocol_copyPropertyList(Protocol * _Nonnull proto,
                            unsigned int * _Nullable outCount)
          OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
      
      
      • 返回的是属性列表,列表中的每个元素都是一个 objc_property_t 指针
      @interface Person ()
      
      @property (nonatomic, strong) NSString *name;
      @property (nonatomic, assign) int age;
      @property (nonatomic, assign) double weight;
      
      @end
      
      // 写 person 添加3个属性。通过 runtime 获取运行时属性。
      
      unsigned int outCount = 0;
          
      objc_property_t *properties = class_copyPropertyList([Person class], &outCount);
          
      NSLog(@"%d", outCount);
          
      for (NSInteger i = 0; i < outCount; ++i) {
          NSString *name = @(property_getName(properties[i]));
          NSString *attributes = @(property_getAttributes(properties[i]));
          NSLog(@"name:%@\nattributes:%@", name, attributes);
      }
      
      [10522:615669] 4
      [10522:615669] name:name
      attributes:T@"NSString",&,N,V_name
      [10522:615669] name:age
      attributes:Ti,N,V_age
      [10522:615669] name:weight
      attributes:Td,N,V_weight
          
      

    runtime 与消息

    • 消息知道运行时才会与方法实现进行绑定。
    • objc_msgSend 方法看起来好像返回了数据,其实 objc_msgSend 从不返回数据,而是你的方法在运行时实现被调用后才会返回数据。消息发送步骤:
      • 首先你要检测 selector 是不是要忽略。mac 开发有了垃圾回收旧不理会 retain、release 这些函数。
      • 检测这个 selector 的 target 是不是 nil。objc 允许我们对一个 nil 对象执行任何方法不会 crash,因为运行时会被忽略掉。
      • 如果上面两步都通过了,那么就开始查找这个类的实现 IMP,先从 cache 中找,如果找到了就运行对应的函数去执行相应的代码。
      • 如果 cache 找不到就找类的方法列表中是否有对应的方法。
      • 如果累的方法列表中找不到就到父类的方法列表中找,一直找到 NSObject 类为止。
      • 如果还没找到,就要开始进入动态方法解析了。
    • 在消息传递中,编译器会根据情况在 objc_msgSend,objc_msgSend_stret,objc_msgSendSuper,objc_msgSendSuper——stret 这个四个方法中选择一个调用。如果消息传递给父类,那么会调用名字带有 Super 的函数,如果消息返回值是数据结构而不是简单值时,会调用带有 stret 的函数。

    方法中的隐藏参数

    • 疑问:我们经常用到关键字 self,但是 self 是如何获取当前方法的对象的呢?其实这也是 runtime 系统的作用,self 是在方法运行时被动态传入的。
    • 当 objc_msgSend 找到方法对应实现时,他将直接调用该方法实现,并将消息中所有参数都传递给方法实现,同时还有两个隐藏参数:
      • 接受消息的对象(self 所指向的内容,当前方法的对象指针)
      • 方法选择器(_cmd 指向的内容,当前指针的 SEL 指针)
      • 因为在源代码方法的定义中,我们并没有发现这两个参数的声明。它们实在代码编译阶段被插入方法实现中的。尽管这些参数没有被明确声明,在源码中我们仍然可以引用它们。
      • 这两个参数中,self 更实用。他是在方法实现中访问消息接收者对象的实例变量的途径。
    • 这时我们会想到另一个关键字 Super,实际上 Super 关键字接收消息时,编译器会创建一个 objc_super 结构体

    消息转发

    • 重定向
      • 消息转发机制执行前,runtime 系统允许我们替换消息的接收者为其他对象。通过- (id)forwardingTargetForSelector:(SEL)aSelector 方法。
      • 如果返回为 nil 或者 self,则会计入消息转发机制(forwardInvocation:),否则向返回的对象重新发送消息。
    • 转发
      • 当动态方法解析不做处理返回 NO 时,则会触发消息转发机制。

    动态绑定

    • 在运行时确定要调用的方法,动态绑定将调用方法的确定也推迟到运行时。在编译
      时,方法的调用并不和代码绑定在一起,只有在消实发送出来之后,才确定被调用的
      代码。通过动态类型和动态绑定技术,代码每次执行都可以得到不同的结果。运行时
      因子负责确定消息的接收者和被调用的方法。运行时的消息分发机制为动态绑定提供
      支持。当向一个动态类型确定了的对象发送消息时,运行环境系统会通过接收者的isa
      指针定位对象的类,并以此为起点确定被调用的方法,方法和消息是动态绑定的。而
      且,不必在0bjective-C 代码中做任何工作,就可以自动获取动态绑定的好处。在每次发送消息时,特别是当消息的接收者是动态类型已经确定的对象时,动态绑定就会例行而透明地发生

    相关文章

      网友评论

          本文标题:RunTime

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