RunTime

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)