美文网首页
Objective-C Runtime 运行时之一: 类与对象及

Objective-C Runtime 运行时之一: 类与对象及

作者: __season____ | 来源:发表于2018-12-27 01:36 被阅读0次

    目录:

    1. 什么是Runtime?
    2. 与 Runtime 系统进行交互
    3. Runtime 类与对象
      1. 1 类与对象基础数据结构
      • 3.1.1. 类对象(class object)
      • 3.1.2. 实例对象(id/objc_object)
      • 3.1.3. 元类(Metaclass)
    • 3.2. 类与对象操作函数

      • 3.2.1. 类相关操作函数
        • 3.2.1.1. 针对类的相关操作函数
        • 3.2.1.2. 获取类定义
        • 3.2.1.3. 成员变量(ivars)及属性
        • 3.2.1.4 方法(methodLists)
        • 3.2.1.5. 协议(objc_protocol_list)
        • 3.2.1.6. 其它
        • 3.2.1.7. 实例(Example)
      • 3.2.2. 动态创建类和对象
      • 3.2.3. 实例操作函数
      • 3.2.4. 获取类定义
    4. 消息
    • 4.1. 获得方法地址
    • 4.2. 消息发送机制(objc_msgSend函数 )
    • 4.3. 使用隐藏的参数
    5. 消息转发
    • 5.1. 动态方法解析 (resolveInstanceMethod:)
    • 5.2. 备用接收者 (forwardingTargetForSelector:)
    • 5.3. 完整的消息转发
    6. 消息转发和多重继承
    7. 消息转发和类继承

    第一章.什么是Runtime

    Objective-C 语言 将很多操作尽可能的从 编译链接推迟运行时。只要有可能,Objective-C 总是使用动态的方式来解决问题。这意味着 Objective-C 语言不仅需要一个编译器,同时也需要一个运行时系统来执行编译好的代码。这儿的运行时系统扮演的角色类似于 Objective-C 语言的操作系统,Objective-C 基于该系统来工作

    运行时系统 是一个公开接口的动态库,有一些数据结构函数的集合组成,这些数据结构和函数的声明 头文件 存放于 /usr/include/objc目录下,这些函数支持用纯C的函数来实现和Objective-C同样的功能。还有一些函数构成了NSObject 类方法的基础。这些函数使得访问运行时系统接口和提供开发工具成为可能,这意味着我们使用时只需要引入 objc/Runtime.h头文件即可

    常见面试题:

    1.说说什么是runtime?平时项目中有用过么?
    2.了解runtime吗?是什么?

    参考答案:(自己总结的,若有误或有好的答案,请留言
    runtime 简称运行时,是系统在运行的时候的一些机制,其中最主要的是消息机制,由一些数据结构函数的集合组成,是一套纯C写的API。Objective-C 基于运行时系统来工作。因为Objective-C 语言允许很多操作尽可能的从编译和链接时推迟到运行时。只要有可能,Objective-C 总是使用动态的方式来解决问题。这意味着 Objective-C 语言不仅需要一个编译器,同时也需要一个运行时系统来执行 编译好的代码。这儿的运行时系统扮演的角色类似于 Objective-C 语言的操作系统

    3.为什么需要Runtime?

    参考答案:(自己总结的,若有误或有好的答案,请留言
    Objective-C 是一门动态性比较强的编程语言,它会将一些工作放在代码运行时才处理而并非编译时。而在运行时,我们所编写的OC代码会转换成完整的运行时代码。OC的函数调用称为消息发送,属于动态调用过程。在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。因此,编译器是不够的,我们还需要一个运行时系统(Runtime system)来处理编译后的代码。

    第二章.与 Runtime 系统进行交互

    Objective-C 程序有三种途径和运行时系统交互:

    1. 通过Objective-C源代码
    2. 通过类NSObject的方法
    3. 通过运行时系统的函数
    • 2.1 通过Objective-C源代码

    大部分情况下,运行时系统在后台自动运行,我们只需编写和编译 Objective-C 源代码。当您编译 Objective-C 类 和 方法时,编译器为实现语言动态特性自动创建一些 数据结构 和 函数。这些数据结构包含 类定义协议类定义中的信息,如在Objective-C 2.0 程序设计语言中定义类和协议类一节所讨论 的 类的对象协议类的对象方法选标实例变量模板,以及其它来自于源代码的信息

    运行时系统的主要功能 就是根据源代码中的表达式发送消息,如"消息”一节所述。

    • 2.2 通过类NSObject的方法

    Cocoa程序中绝大部分类都是NSObject类的子类,所以大部分都继承了NSObject类的方法,因而继承 了NSObject的行为。(NSProxy类是个例外;更多细节参考““消息转发”一节。)然而,某些情况下,NSObject仅仅 定义了完成某件事情的模板,而没有提供所有需要的代码。例如,NSObject 类定义了 description方法,返回该类内容的字符串表示,该方法主要是用来调试程序 (GDB 中的 print-object 方法就是直接打印出该方法返回的字符串)。NSObject 类中 该方法(description)的实现 并不知道子类中的内容,所以它只是返回类的名字和对象的地址。那 NSObject的子类可以重新实现该方法,以提供更多的信息。例如,NSArray 类改写了该方法来返回 NSArray 类包含的每个对象的内容。

    还有一些 NSObject 的方法只是简单地从 Runtime 系统中获取信息,从而允许对象进行一定程度的自我检查。例如:

    1. -class方法 : 返回对象的类;
    2. -isKindOfClass:-isMemberOfClass:方法 : 检查对象是否存在于指定的类的继承体系中(也可以说成是判断 是否是其子类或者父类或者当前类的成员变量);
    3. -respondsToSelector: : 检查对象能否响应指定的消息;
    4. -conformsToProtocol: : 检查对象是否实现了指定协议类的方法;
    5. -methodForSelector: : 返回指定方法实现的地址。
    • 2.3 通过运行时系统的函数

    尽管大部分情况下运行时系统的函数在 Objective-C 程序不是必须的,但是有时候对于 Objecitve-C 程序来说某些函数是非常有用的。 这些函数的文档参见 Objective-C Runtime Reference,也就是 Runtime API 文档。

    第三章. Runtime 类与对象

    3.1 类与对象基础数据结构

    • 3.1.1. 类对象(class object)

    Objective-C的是由Class 类型来表示的,它实际上是一个指向objc_class结构体指针。在 objc.h 和 runtime.h 中找到对 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 结构体的各成员介绍如下:

    isa:是一个 objc_class 类型的指针,它指向metaClass(元类)。所有的类自身也是一个对象,那对象对象类自身也是对象,是不是有点混淆?

    别急,Objective-C 中 一个术语来区分这两种不同的对象:

    • <1>类对象(class object)与<2>实例对象(instance object)

    Objective-C还对类对象实例对象中的isa所指向的类结构作了不同的命名:

    • 1>. 类对象(objc_class)中的 isa 指向类结构被称作元类(Metaclass)元类(Metaclass)就是类对象的类,每个类都有自己的元类,每个元类又有自己的isa,metaclass存储类的static类成员变量static类成员方法+开头的方法),这也是Objective-C的类方法使用元类的根本原因;
    • 2>. 实例对象中的 isa 指向类结构称作 class普通的class 结构存储类的普通成员变量普通成员方法-开头的方法**)。

    super_class:指向该类的父类!如果该类已经是最顶层的根类(如 NSObject 或 NSProxy),那么 super_class 就为 NULL

    struct objc_super {
        //指定类的实例.
        __unsafe_unretained _Nonnull id receiver;
    
        //指定特定的消息实例的超类. 
    #if !defined(__cplusplus)  &&  !__OBJC2__
        /* For compatibility with old objc-runtime.h header */
        __unsafe_unretained _Nonnull Class class;
    #else
        __unsafe_unretained _Nonnull Class super_class;
    #endif
        /* super_class是第一个要搜索的类 */
    };
    

    name:一个 C 字符串,指示类的名称。我们可以在运行期,通过这个名称查找到该类(通过:id objc_getClass(const char *aClassName))或该类的 metaclass(id objc_getMetaClass(const char *aClassName));

    version类的版本信息,默认初始化为 0。我们可以在运行期对其进行修改(class_setVersion)或获取(class_getVersion)。

    info:类信息,供运行期使用的一些位标识。有如下一些位掩码:

    CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含实例方法和变量;
    CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
    CLS_INITIALIZED (0x4L) 表示该类已经被运行期初始化了,这个标识位只被 objc_addClass 所设置;
    CLS_POSING (0x8L) 表示该类被 pose 成其他的类;(poseclass 在ObjC 2.0中被废弃了);
    CLS_MAPPED (0x10L) 为ObjC运行期所使用

    CLS_FLUSH_CACHE (0x20L) 为ObjC运行期所使用

    CLS_GROW_CACHE (0x40L) 为ObjC运行期所使用

    CLS_NEED_BIND (0x80L) 为ObjC运行期所使用

    CLS_METHOD_ARRAY (0x100L) 该标志位指示 methodlists 是指向一个 objc_method_list 还是一个包含 objc_method_list 指针的数组;

    instance_size:该类的实例变量大小(包括从父类继承下来的实例变量);

    ivars:指向 objc_ivar_list的指针,存储每个实例变量的内存地址,如果该类没有任何实例变量则为 NULL;

    //该类的成员变量链表,是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;
    }        
    

    Ivar是表示实例变量的类型,其实际是一个指向objc_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
    }  
    

    methodLists与 info 的一些标志位有关CLS_METHOD_ARRAY 标识位决定其指向的东西(是指向单个 objc_method_list还是一个 objc_method_list 指针数组),如果 info 设置了 CLS_CLASSobjc_method_list 存储实例方法,如果设置的是 CLS_META 则存储类方法

    方法链表结构体:

    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;
    }                                                            OBJC2_UNAVAILABLE;                                      
    

    对象的每个方法的结构体,SEL是方法选择器,是HASH后的值,可以通过这个值找到函数体的实现,IMP 是函数指针

    struct objc_method {  
        SEL _Nonnull method_name    //SEL是方法选择器                                   OBJC2_UNAVAILABLE;  
        char *_Nullable method_types   //方法类型(参数列表),
    // `method_types `是个char 指针, `存储`方法的`参数类型`和`返回值类型`。                               OBJC2_UNAVAILABLE;  
        IMP _Nonnull method_imp   //method_imp 指向了`方法的实现`,
      //本质是一个`函数指针`。                                    OBJC2_UNAVAILABLE;  
    }   OBJC2_UNAVAILABLE
      
    

    cache:指向 objc_cache 的指针,用来缓存最近使用的方法,以提高效率。一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差。这时,cache就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法。这样,对于那些经常用到的方法的调用,提高了调用的效率

    它用于缓存调用过的方法。这个字段是一个指向objc_cache结构体的指针,其定义如下:
    
    struct objc_cache {
        unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
        unsigned int occupied                                    OBJC2_UNAVAILABLE;
        Method buckets[1]                                        OBJC2_UNAVAILABLE;
    };
    

    该结构体的字段描述如下:

    • mask:一个整数,指定分配的缓存bucket的总数。在方法查找过程中,Objective-C runtime 使用这个字段来确定开始线性查找数组的索引位置。指向方法selector的指针与该字段做一个AND位操作(index = (mask & selector))。这可以作为一个简单的hash散列算法。
    • occupied:一个整数,指定实际占用的缓存bucket的总数
    • buckets:指向Method数据结构指针的数组。这个数组可能包含不超过mask+1个元素。需要注意的是,指针可能是NULL,表示这个缓存bucket没有被占用,另外被占用的bucket可能是不连续的。这个数组可能会随着时间而增长。

    protocols:指向 objc_protocol_list 的指针,存储该类声明要遵守的正式协议

    • 3.1.2. 实例对象(id/objc_object)

    id指向一个类的实例对象(objc_object)的指针。id被定义在 objc/objc.h 目录下,它的数据结构是:

    typedef struct objc_object *id;
    

    其中 objc_object的底层定义

    struct objc_object {
        Class isa  OBJC_ISA_AVAILABILITY;
    };
    

    可以看到,iOS中很重要的id实际上就是objc_object的指针.而NSObject的第一个对象就是Class类型的isa。因此 id可以标示所有基于NSObject的对象对象可以通过 isa 指针找到其所属的类isa 是一个 Class 类型成员变量

    这样,当我们向一个Objective-C对象发送消息时,Runtime会根据实例对象的isa指针找到这个实例对象所属的类。然后,Runtime会在类的方法列表或者是父类的方法列表中去寻找与消息对应的selector指向的方法,找到后即运行这个方法。

    注意: isa 指针在代码运行时并不 总指向实例对象所属的类型,所以不能依靠它来确定类型,要想确定类型还是需要用对象的 -class 方法。

    • 3.1.3. 元类(Metaclass)

    meta-class是一个类对象的类。当我们向一个对象发送消息时,runtime会在这个对象所属的这个类的方法列表(-号方法列表)中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表(+号方法列表)中查找。

    meta-class之所以重要,是因为它存储着一个类的所有类方法(+号方法列表)。每个类都会有一个单独的meta-class,因为每个类的类方法基本不可能完全相同。

    再深入一下,meta-class也是一个类,也可以向它发送一个消息,那么它的isa又是指向什么呢?为了不让这种结构无限延伸下去, ObjC的设计者让所有的meta-classisa指向基类(·根类·)的meta-class,以此作为它们的所属类。即,任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类(·根类·)的meta-classisa指针指向它自己。这样就形成了一个完美的闭环。

    通过上面的描述,再加上对objc_class结构体中super_class指针的分析,我们就可以描绘出类及相应meta-class类的一个继承体系了。子类父类根类(这些都是普通 class)以及其对应metaclassisasuper_class 之间关系 如下图所示

    instance object,class,metaclass 的 isa 与 super_class 关系图
    • 规则一:类的实例对象的 isa 指向该类该类的 isa 指向该类的 metaclass
      理解Classobjc_objectisa 指向该 Class 类,该类(objc_class)的isa指向Class metaClass`)
    • 规则二:类的 super_class指向其父类,如果该类为根类则值为NULL
      理解Classsuper_class 指向该Class父类,如果该类Class最顶层的类,那么该Classsuper_class 值为NULL)
    • 规则三:metaclassisa 指向根 metaclass,如果该 metaclass根 metaclass指向自身
      理解不管是子类还是父类metaclassisa 一律 都指向 根 metaclass,如果该 metaclass 是根 metaclass指向自身)
    • 规则四:metaclasssuper_class 指向父 metaclass,如果该 metaclass 是根 metaclass则指向该 metaclass 对应的类,也就是指向根类不是自身metaclass

    注意:根元类的superclass不是nil而是根类。对于ObjC原生的类,根元类父类就是系统的根类NSOject。但根类不一定是NSObject,因为后面介绍的objc_allocateClassPair函数也可创建出一个根类。

    那么 class 与 metaclass 有什么区别呢?

    classinstance object类 类型。当我们向实例对象发送消息实例方法)时,我们在该实例对象class 结构methodlists方法列表 中去查找响应的函数,如果没找到匹配的响应函数则在该 class父类中的 methodlists 方法列表去查找(查找链为上图的中间那一排)。如下面的代码中,向str 实例对象发送 lowercaseString 消息,会在 NSString 类结构(NSString 的objc_class)的 methodlists 方法列表中去查找lowercaseString 的响应函数。

    NSString * str;
    [str lowercaseString];
    
    

    metaclassclass object类 类型 。当我们向类对象发送消息(类方法)时,我们在该类对象的 metaclass(元类) 结构methodlists 中去查找响应的函数,如果没有找到匹配的响应函数则在该 metaclass 的父类中的 methodlists去查找(查找链为上图的最右边那一排)。如下面的代码中,向 NSString 类对象发送 stringWithString 消息,会在 NSStringmetaclass 类结构的 methodlists 中去查找 stringWithString 的响应函数。

    [NSString stringWithString:@"str"];  //stringWithString 是 + 号方法
    

    好,至此我们明白了类的结构层次,来看张图会更明显:

    类存储示意图

    总结
    ObjC 为每个类的定义生成两个 objc_class ,一个即普通的 class,另一个即 metaclass。我们可以在运行时创建 这两个 objc_class 数据结构,然后使用 objc_addClass 动态地创建新的类定义。**这个够动态够强大的吧?

    3.2. 类与对象操作函数

    runtime提供了大量的函数来操作类与对象。类的操作方法大部分是以class_为前缀的,而对象的操作方法大部分是以objc_object_为前缀。下面我们将根据这些方法的用途来分类讨论这些方法的使用。

    • 3.2.1. 类相关操作函数

    我们可以回过头去看看objc_class的定义,runtime提供的操作类的方法主要就是针对这个结构体中的各个字段的。下面我们分别介绍这一些的函数。并在最后以实例来演示这些函数的具体用法。

    • 3.2.1.1 针对类进行操作的函数

    针对类进行操作的函数主要有:

    /**
      *返回类的名称。
     *
      * @param cls一个类对象。
      * @return 类的名称,如果 cls是 Nil,则为空字符串。
     */
    //获取类的类名
    const char * class_getName ( Class cls );
    
    /**
      *返回类的超类。
      * @param cls一个类对象。
      * @return类的超类,或者Nil if cls是根类,如果cls是Nil,则为Nil。
      * @note你 通常应该使用NSObject的超类方法 ,而不是这个函数。
     */
    // 获取类的父类
    Class class_getSuperclass ( Class cls );
    
    /**
      *返回一个布尔值,指示类对象是否是元类。
      * @param cls一个类对象。
      * @return YES如果cls是元类,如果cls是非元类,则为NO,如果cls为Nil,则为NO。
     */
    // 判断给定的Class是否是一个元类
    BOOL class_isMetaClass ( Class cls );
    
    //返回类的实例大小
    size_t
    class_getInstanceSize(Class _Nullable cls);
    
    /**
      *返回给定类的指定 实例变量的Ivar 。
     *
      * @param cls 您希望获取其实例变量的类。
      * @param name 要获取的实例变量定义的名称。
      * @return 指向包含有关信息的Ivar数据结构的指针
      * 由name指定的实例变量。
     */
    // 返回给定类的指定实例变量的Ivar
    Ivar class_getInstanceVariable(Class  cls, const char *  name); 
    
    /**
      *返回给定类的指定 类变量 的Ivar。
     *
      * @param cls 您希望获取其 类变量 的类定义。
      * @param name 要获取的 类变量 定义的名称。
      * @return 返回一个指向Ivar数据结构的指针,该结构包含有关name指定的  类变量 的信息。
     */
    // 返回 给定类的指定类变量的Ivar
    Ivar class_getClassVariable ( Class cls, const char *name );
    
    /**
      *向类添加新的实例变量。
     *  
      * @ cls 你要操作的类名
      * @ name 成员变量名称
      * @ size 开辟字节长度
      * @ alignment 对齐方式
      * @ types 参数类型 “@”
      * @return 如果成功添加了实例变量,则为YES,否则为NO
      *(例如,该类已包含具有该名称的实例变量)。
      * @note: 只能在 objc_allocateClassPair 之后和 objc_registerClassPair 
        之前调用此函数。不支持 将实例变量添加到 现有类。
      * @note : 该类 不能是元类 。 不支持将实例变量添加到元类。
      * @note:实例变量的最小对齐字节数为1 << align。 实例的最小对齐方式
      *变量取决于ivar的类型和机器架构。
      *对于任何指针类型的变量,传递log2(sizeof(pointer_type))。
     */
    
    // 向类添加新的实例变量
    BOOL class_addIvar ( Class cls, const char *name, size_t size, 
                           uint8_t alignment, const char *types );
    
    /**
      *描述类声明的 实例变量列表。
     *
      * @param cls要操作类。
      * @param outCount返回时,包含返回数组的长度。
      *如果outCount为NULL,则不返回长度。
      * @return 类型为  Ivar的指针数组  ,描述类声明的实例变量。
      *  不包括超类  声明的  任何实例变量。 该数组包含* outCount
      *指针后跟一个NULL终止符。 您必须使用free() 释放数组。
      *如果类声明没有实例变量,或者cls为Nil,则返回NULL并且* outCount为0。
     */
    // 获取整个实例变量列表,获取的不仅有@property声明的变量还有不是@property声明的变量。
     Ivar  * class_copyIvarList(Class  cls, unsigned int *  outCount) ;
    
    /**
      *设置给定类的Ivar布局。
     *
      * @param cls要修改的类。
      * @param layout  cls的 Ivars的布局。
     */
    void class_setIvarLayout ( Class cls, const uint8_t *layout );
    
    /**
      *返回给定类的 Ivar布局的描述。
     *
      * @param cls要检查的类。
      * @return  cls的  Ivar布局的描述。
     */
    const uint8_t * class_getIvarLayout ( Class cls );
    
    /**
      *为给定的类设置弱Ivars的布局。
     *
      * @param cls要修改的类。
      * @param layout cls的弱Ivars布局。
     */
    void class_setWeakIvarLayout ( Class cls, const uint8_t *layout );
    
    /**
      *返回给定类的弱Ivars布局的描述。
     *
      * @param cls要检查的类。
      * @return  对 cls的弱 Ivars布局的描述。
     */
    const uint8_t * class_getWeakIvarLayout ( Class cls );
    
    
    /**
      *返回具有给定类的给定名称的属性。
     *
      * @param cls您要操作类。
      * @param name 要操作的属性的名称。
      * @return 返回描述属性的 objc_property_t 类型的 指针,
         如果类没有声明具有该名称的属性,则为NULL;如果cls为Nil,则为NULL。
     */
    // 返回具有给定类的给定名称的属性
    objc_property_t class_getProperty ( Class cls, const char *name );
    
    /**
    描述  类声明 的 属性列表。
    *
    * @param cls是您要操作的类。
    * @param *outCount ,输出指针,指明返回objc_property_t类型数组的大小。
    *如果 outCount 为 NULL,则 不返回长度。
    *@返回 objc_property_t 类型 的  指针数组 ,该数组描述 类声明的属性 。
    不包括超类 声明的任何属性。数组包含*outCount指针,后跟NULL结束符。
     必须使用free()释放数组。
    *
    *如果cls声明没有属性,或者cls为Nil,则返回NULL和*outCount为0。
    */
    
    // 获取类声明 的 属性列表,获取到的只有 @property声明的变量
    objc_property_t * class_copyPropertyList ( Class cls, 
                              unsigned int *outCount );
    
    
    /**
      *向具有 给定名称 和 实现的类 添加 新方法。
     *
      * @param cls 要添加方法的类。
      * @param name 一个选择器,指定要添加的方法的名称的选择器。
      * @param  imp 是新方法的实现函数,它是新方法的实现。 
         该函数必须至少有两个参数 - self和_cmd。
      * @param types 是 一个字符数组,用于描述方法参数的类型。
      * @return YES如果成功添加方法,否则为NO
      *(例如,该类已包含具有该名称的方法实现)。
      * class_addMethod的实现会覆盖父类的方法实现,
      *但不会替换此类中的已存在的实现。
      *要更改现有实现,请使用method_setImplementation。
     */
    //向具有给定名称和实现的类添加新方法
    BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types );
    
    /**
      *返回给定类的指定实例方法。
     *
      * @param cls您要操作的类。
      * @param name 指定要检索的方法的选择器。
      * @return  与指定的选择器的实现 相对应的方法
      * 由 cls指定的类的名称,或  如果指定的类或其为NULL
      *父类不包含具有指定选择器的实例方法。
        此函数在父类中搜索实现,而 class_copyMethodList则不在。
     */
    // 返回给定类的指定实例方法
    Method class_getInstanceMethod ( Class cls, SEL name );
    
    /**
      *返回指向描述给定类的给定类方法的数据结构的指针。
     *
      * @param cls指向类定义的指针。 传递包含要检索的方法的类。
      * @param name类型为 SEL的指针。 传递要检索的方法的选择器。
      * @return 一个指向  方法数据结构的指针,对应于该实现
      *由aSelector指定的选择器,用于由aClass指定的类,如果指定,则为NULL
      *类或其父类不包含具有指定选择器的实例方法。
      * 请注意,此函数在父类中搜索实现,而  class_copyMethodList没有。
     */
    // 获取类方法,根据给定类和给定selector获取Method 的数据结构的指针
    Method class_getClassMethod ( Class cls, SEL name );
    
    /**
      *描述类实现的实例方法。
      * @param  cls您要操作的类。
      * @param  outCount返回时,包含返回数组的长度。
      *如果outCount为NULL,则不返回长度。
     *
      * @return   描述实例方法的Method类型的指针数组
      *由类实现 - 不包括父类实现的任何实例方法。
      *该数组包含* outCount指针,后跟一个NULL终止符。 您必须使用free()释放数组。
     *
      *如果cls没有实现实例方法,或者cls为Nil,则返回NULL并且* outCount为0。
     *
      * 要获取类的类方法,请使用  class_copyMethodList (object_getClass(cls),&count)。
    *
      * 获取可由父类实现的方法的实现,使用 class_getInstanceMethod 
        或 class_getClassMethod。
     */
    // 获取所有实例方法(-号方法)的数组
    Method * class_copyMethodList ( Class cls, unsigned int *outCount );
    
    /**
     *替换给定类的方法的实现。
     *
     * @param cls 您要修改的类。
     * @param name 一个选择器,用于标识要  替换其实现的方法。
     * @param imp 由 cls标识 的  类的名称 标识的方法的新实现。
     * @param types一组字符,用于描述方法参数的类型。
     *由于该函数必须至少有两个参数 - self和_cmd,第二个和第三个字符
     *必须是“@:”(第一个字符是返回类型)。
     *
     * @return  返回 由 cls标识的类的 name标识的方法的先前实现。
     * @note  此函数以两种不同的方式运行:
     * - 如果 name指定的方法尚不存在,则添加它,就像调用了 class_addMethod一样。
     *由 types指定的类型编码用于给定。
     * - 如果由 name标识的方法确实存在,则将其 IMP替换为调用method_setImplementation。
     *忽略类型指定的类型编码。
     */
    // 替代方法的实现,替换给定类的方法的实现。
    IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types );
    
    /**
    返回一个函数指针,如果一个特定的消息被发送到一个类的实例中,该函数指针将被调用。
    *
    * @param cls 你想要操作的类。
    * @param name  一个选择器。
    *
    * @return  一个函数指针,如果用类的实例调用[object name],
      就会调用它;如果cls是Nil,就会调用NULL。
    *
    * class_getMethodImplementation可能比method_getImplementation
    (class_getInstanceMethod, cls, name)更快。
    *
    * @注意返回的函数指针可能是运行时内部的函数,而不是一个实际的方法实现。
    例如,如果类的实例不响应选择器,返回的函数指针将成为运行时消息转发机制的一部分。
    */
    // 返回方法的具体实现 
    IMP class_getMethodImplementation ( Class cls, SEL name );
    
    /** 
    返回一个函数指针,如果一个特定的消息被发送到一个类的实例中,该函数指针将被调用。
    *
    * @param  cls 你想要操作的类。
    * @param name 一个选择器。
    * @return  调用  [object name] 时将调用的函数指针
    *带有类的实例,或者 NULL(如果 cls是 Nil)。
    */
    IMP class_getMethodImplementation_stret ( Class cls, SEL name );
    
    // 类的实例是否响应指定的selector
    BOOL class_respondsToSelector ( Class cls, SEL sel );
    
    /**
      *向类添加属性。
     *
      * @param cls 要修改的类。
      * @param name 属性的名称。
      * @param attributes 属性特性数组。
      * @param attributeCount属性中的属性数。
     *
      * @return如果已成功添加属性,则为YES,否则为NO(例如,类已具有该属性)。
     */
    // 为类添加属性
    BOOL class_addProperty ( Class cls, const char *name, 
     const objc_property_attribute_t *attributes, unsigned int attributeCount );
    
    /**
      *替换类的属性。
     *
      * @param cls要修改的类。
      * @param name属性的名称。
      * @param attributes 属性特性数组。
      * @param attributeCount 属性中的属性数。
     */
    // 替换类的属性
    void class_replaceProperty ( Class cls, const char *name, 
      const objc_property_attribute_t *attributes, unsigned int attributeCount );
    
    // 添加协议
    BOOL class_addProtocol ( Class cls, Protocol *protocol );
    // 返回类是否实现指定的协议
    BOOL class_conformsToProtocol ( Class cls, Protocol *protocol );
    // 返回类实现的协议列表
    Protocol * class_copyProtocolList ( Class cls, unsigned int *outCount );
    
    // 获取版本号
    int class_getVersion ( Class cls );
    // 设置版本号
    void class_setVersion ( Class cls, int version );
    
    
    • class_getInstanceVariable函数:获取 类 中指定名称实例成员变量的信息。它返回一个指向包含name指定的成员变量信息objc_ivar结构体的指针(Ivar)。

    • class_getClassVariable函数:可以获取类成员变量的信息。目前没有找到关于Objective-C中类变量的信息,一般认为Objective-C不支持类变量。注意,返回的列表不包含父类的成员变量和属性。

    • class_addIvar函数:可以添加成员变量Objective-C不支持往已存在的类中添加实例变量,因此不管是系统库提供的类,还是我们自定义的类,都无法动态添加成员变量但如果我们通过运行时来创建一个类的话,又应该如何给它添加成员变量呢?这时我们就可以使用class_addIvar函数了。不过需要注意的是,这个方法只能objc_allocateClassPair函数与objc_registerClassPair之间调用。另外,这个类也不能是元类。成员变量的按字节最小对齐量是1<<alignment。这取决于ivar的类型和机器的架构。如果变量的类型是指针类型,则传递log2(sizeof(pointer_type))。

    • class_copyIvarList函数:可以 获取整个成员变量列表。它返回一个指向成员变量信息的数组,数组中每个元素是指向该成员变量信息的objc_ivar结构体的指针。这个数组不包含在父类中声明的变量。outCount指针返回数组的大小。需要注意的是,我们必须使用free()来释放这个数组

    • class_addMethod函数:向具有给定名称和实现的类添加新方法。class_addMethod的实现会覆盖父类的方法实现,但不会取代本类中已存在的实现,如果本类中包含一个同名的实现,则函数会返回NO。如果要修改已存在实现,可以使用method_setImplementation。一个Objective-C方法是一个简单的C函数,它至少包含两个参数–self和_cmd。所以,我们的实现函数(IMP参数指向的函数)至少需要两个参数,如下所示:

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

    成员变量不同的是我们可以为类动态添加方法不管这个类是否已存在

    另外,参数types是一个描述传递给方法的参数类型的字符数组,这就涉及到类型编码

    • class_getInstanceMethod 获取实例方法函数、class_getClassMethod 获取类方法函数,与class_copyMethodList 获取所有方法的数组不同的是,这两个函数都会去搜索父类的实现

    • class_copyMethodList函数:获取所有方法的数组。返回包含所有实例方法的数组
      如果需要·获取类方法·,则可以使用class_copyMethodList(object_getClass(cls), &count)(一个类的实例方法定义在元类里面)。该列表不包含父类实现的方法。outCount参数返回方法的个数。在获取到列表后,我们需要使用free()方法来释放它

    • class_replaceMethod函数:替代方法的实现
      该函数的行为可以分为两种:

      • 如果类中不存在name指定的方法,则类似于class_addMethod函数一样会添加方法;
      • 如果类中已存在name指定的方法,则类似于method_setImplementation一样 修改已存在实现替代原方法的实现
    • class_getMethodImplementation 函数 :返回方法的具体实现。该函数在向类实例发送消息时会被调用,并返回一个指向方法实现函数指针。这个函数会比method_getImplementation(class_getInstanceMethod(cls, name))更快。返回的函数指针可能是一个指向runtime内部的函数,而不一定是方法的实际实现。例如,如果类实例无法响应selector,则返回的函数指针将是运行时消息转发机制的一部分

    • class_respondsToSelector函数,我们通常使用NSObject类的respondsToSelector:instancesRespondToSelector:方法来达到相同目的。

    • class_conformsToProtocol 函数:返回 类是否实现指定的协议。你通常应该使用NSObject类的conformsToProtocol:方法来替代,而不是这个函数。

    • class_copyProtocolList 函数:返回 类实现的协议列表。在使用后我们需要使用free()手动释放

    • 在MAC OS X系统中**,我们可以使用垃圾回收器。runtime提供了class_setIvarLayoutclass_getIvarLayoutclass_setWeakIvarLayoutclass_getWeakIvarLayout、几个函数来确定一个对象的内存区域是否可以被垃圾回收器扫描,以处理strong/weak引用。但通常情况下,我们不需要去主动调用这些方法;在调用objc_registerClassPair时,会生成合理的布局。在此不详细介绍这些函数

    • 3.2.1.2 获取类定义

    Objective-C动态运行库会自动注册我们代码中定义的所有的类。我们也可以在运行时创建类定义并使用objc_addClass函数来注册它们。runtime提供了一系列函数来获取类定义相关的信息,这些函数主要包括:

    // 获取已注册的类定义的列表
    int objc_getClassList ( Class *buffer, int bufferCount );
    // 创建并返回一个指向所有已注册类的指针列表
    Class * objc_copyClassList ( unsigned int *outCount );
    
    /**
      *返回指定类的类定义。
     *
      * @param name 要查找的类的名称。
      * @return 指定类的Class对象,如果该类未在Objective-C运行时注册,
          则为nil。
      * @note: objc_getClass 与此函数的不同之处在于: 
             1.如果未注册类,则 objc_getClass 调用类处理程序回调,
             2.然后再次检查以查看该类是否已注册。 
             3. 此函数不会调用类处理程序回调。
     */
    // 返回指定类的类定义
    Class objc_lookUpClass ( const char *name );
    Class objc_getClass ( const char *name );
    /**
      *返回指定类的类定义。
     *
      * @param name要查找的类的名称。
      * @return指定类的Class对象。
      * @note :此函数与 objc_getClass相同,但如果找不到类,则会终止进程。
      * @note ZeroLink使用此函数,如果没有找到类,
             则会出现没有ZeroLink的编译时链接错误。
     */
    Class objc_getRequiredClass ( const char *name );
    // 返回指定类的元类
    Class objc_getMetaClass ( const char *name );
    
    • 获取类定义的方法有三个:objc_lookUpClass, objc_getClassobjc_getRequiredClass。如果类在运行时未注册,则objc_lookUpClass会返回nil,而objc_getClass会调用类处理回调,并再次确认类是否注册,如果确认未注册,再返回nil。而objc_getRequiredClass函数的操作与objc_getClass相同,只不过如果没有找到类,则会杀死进程

    • objc_getMetaClass函数:如果指定的类没有注册,则该函数会调用类处理回调,并再次确认类是否注册,如果确认未注册,再返回nil。不过,每个类定义都必须有一个有效的元类定义,所以这个函数总是会返回一个元类定义,不管它是否有效

    • objc_getClassList函数:获取已注册类定义的列表。我们不能假设从该函数中获取的类对象是继承自NSObject体系的,所以在这些类上调用方法是,都应该先检测一下这个方法是否在这个类中实现。

    由于篇幅太长,不能写在一篇文章中,具体详情参考:Objective-C Runtime 运行时之二:成员变量、属性和方法

    • 3.2.2. 动态创建类和对象

    runtime的强大之处在于它能在运行时创建类和对象
    我们来如何在运行
    时动态创建类。下面这个函数就是应用前面讲到的Class,MetaClass的概念,在运行时动态创建一个类。这个函数来自《Inside Mac OS X-The Objective-C Programming Language》。

    • 1>.动态创建类

    动态创建类 涉及到以下几个函数:

    /**
     *创建一个新类和元类。
     *
     * @param superclass 用作新类的父类的类,或者 Nil用于创建新的根类。
     * @param name 用作新类名的字符串。该字符串将被复制。
     * @param extraBytes 在类和元类对象的末尾为索引的ivars分配的字节数。通常应该是 0。
     *
     * @return 新类,如果无法创建类,则为Nil(例如,所需的名称已在使用中)。
     *
     * @note 您可以通过调用 object_getClass(newClass)来获取指向新元类的指针。
     * @note要创建一个新类,请先调用 objc_allocateClassPair。
     *然后使用 class_addMethod和  class_addIvar等函数设置类的属性。
     *完成构建类后,请调用  objc_registerClassPair。新类现在可以使用了。
     * @note应将实例方法和实例变量添加到类本身。
     *类方法应添加到元类中。
     */
    // 创建一个新类和元类
    Class objc_allocateClassPair ( Class superclass, const char *name, 
                                                   size_t extraBytes );
    
    // 销毁一个类及其相关联的类
    void objc_disposeClassPair ( Class cls );
    
    // 在应用中注册由objc_allocateClassPair创建的类
    void objc_registerClassPair ( Class cls );
    
    

    为了创建一个新类,我们需要调用objc_allocateClassPair。然后使用诸如class_addMethodclass_addIvar等函数来为新创建的类添加方法实例变量属性等。完成这些后,我们需要调用objc_registerClassPair函数来注册类,之后这个新类就可以在程序中使用了。

    实例方法实例变量应该添加到类自身上,而类方法应该添加到类的元类上。

    • objc_disposeClassPair函数:用于销毁一个类。不过需要注意的是,如果程序运行中还存在类或其子类的实例,则不能调用针对类调用该方法。

    在前面介绍元类时,我们已经有接触到这几个函数了,在此我们再举个实例来看看这几个函数的使用。

    实例(Example)
    //-----------------------------------------------------------
    @interface MyClass : NSObject  <NSCopying, NSCoding>
    - (void)beReplacedMethod;
    @end
    //-----------------------------------------------------------
    // MyClass.m
    #import "MyClass.h"
    @interface MyClass () 
    
    @end
    @implementation MyClass
    - (void)beReplacedMethod {
        NSLog(@"Adds an IMP to your new method in a new class");
    }
    
    @end
    //-----------------------------------------------------------
    - (void)viewDidLoad {
    
        MyClass *myClass = [[MyClass alloc] init];
        
        Class cls = myClass.class;
        // 类名
        NSLog(@"class name: %s", class_getName(cls));
        NSLog(@"=====================================");
        // 父类
        NSLog(@"The name of the superclass of MyClass is: %s", class_getName(class_getSuperclass(cls)));
        NSLog(@"=====================================");
    
        // 是否是元类
        NSLog(@"MyClass is %@ a meta-class", (class_isMetaClass(cls) ? @"" : @"not"));
        NSLog(@"======================================");
        
        Class meta_class = objc_getMetaClass(class_getName(cls));
        NSLog(@"%s's meta-class is %s", class_getName(cls), class_getName(meta_class));
        NSLog(@"======================================");
        
        // 变量实例大小
        NSLog(@"instance size: %zu", class_getInstanceSize(cls));
        NSLog(@"======================================");
    
    // 创建一个新类和元类 MyClass.class为新类MySubClass的父类
        Class cls = objc_allocateClassPair(MyClass.class, "MySubClass", 0);
        
    /*
       您必须使用Objective-C运行时系统注册方法名称才能
       获得方法的选择器,然后才能将方法添加到类定义中。
       如果方法名称已经注册,此函数只返回选择器。
    */
         SEL sel =  sel_registerName ("beReplacedMethod");
        
    // class_getMethodImplementation 返回方法的具体实现
    //获取 MyClass 类中 beReplacedMethod 方法选择器的 方法实现地址IMP
        IMP imp = class_getMethodImplementation([MyClass class],sel );
        
    //给新创建的 MySubClass 类  添加名称为 findInSelf的新方法,imp 是新方法的方法实现地址
        class_addMethod(cls, @selector(findInSelf),imp , "v@:");
        
    /**
     * @note: 只能在 objc_allocateClassPair 之后和 objc_registerClassPair
     之前调用此函数。不支持 将实例变量添加到 现有类。
     * @note : 该类 不能是元类 。 不支持将实例变量添加到元类。
     */
    // 添加实例变量
        class_addIvar(cls, "_ivar1", sizeof(NSString *), 
                                        log(sizeof(NSString *)), "i");
        
        //属性的特性(attribute)
        objc_property_attribute_t  type = {"T", "@\"NSString\""};
        //属性的特性(attribute)
        objc_property_attribute_t  ownership = { "C", "" };
        //属性的特性(attribute)
        objc_property_attribute_t  backingivar = { "V", "_ivar1"};
        //属性的特性数组
        objc_property_attribute_t  attrs[] = {type, ownership, backingivar};
    
        // 为类添加属性
        class_addProperty(cls, "property2", attrs, 3);
        
        // 在应用中注册由objc_allocateClassPair创建的类
        objc_registerClassPair(cls);
        
    //-----------------------------------------------------------   
        //开始调用
        id instance = [[cls alloc] init];
        
        //在这里使用 performSelector 来向新类的对象发送消息,
        //可以避免编译警告信息(因为我们并没有声明该类及其可响应的消息)
        [instance performSelector:@selector(findInSelf)];
    }
    

    程序的输出如下:

    class name: MyClass
    The name of the superclass of MyClass is: NSObject
    MyClass is not a meta-class
    MyClass's meta-class is MyClass
    instance size: 48
    
    Adds an IMP to your new method in a new class
    
    • 2>.动态创建对象

    动态创建对象的函数如下:

    // 创建类实例,为该类中的类分配内存,默认的malloc内存区域
    id class_createInstance ( Class cls, size_t extraBytes );
    // 在指定位置创建类实例
    id objc_constructInstance ( Class cls, void *bytes );
    // 销毁类实例
    void * objc_destructInstance ( id obj );
    
    • class_createInstance函数:创建实例时,会在默认的内存区域为类分配内存,调用class_createInstance的效果与+alloc方法类似。extraBytes参数表示分配的额外字节数。这些额外的字节可用于存储在类定义中所定义的实例变量之外实例变量该函数在ARC环境下无法使用
    实例操作函数

    实例操作函数主要是针对我们创建的实例对象的一系列操作函数,我们可以使用这组函数来从实例对象中获取我们想要的一些信息,如实例对象中变量的值。

    // 返回指定对象的一份拷贝
    id object_copy ( id obj, size_t size );
    // 释放指定对象占用的内存
    id object_dispose ( id obj );
    
    /**
      *更改类实例的实例变量的值。
     *
      * @param obj 指向类实例的指针。 传递包含要修改其值的实例变量的对象。
      * @param name :一个C字符串。 传递要修改其值的实例变量的名称。
      * @param value: 实例变量的新值。
      * @return : 指向Ivar数据结构的指针,该结构定义由name指定的实例变量的类型和名称。
      * @note:  具有已知内存管理的实例变量(例如ARC强和弱)使用该内存管理。 
         分配具有未知内存管理的实例变量,就好像它们是unsafe_unretained一样。
     */
    
    // 修改类实例的实例变量的值
    Ivar object_setInstanceVariable ( id obj, const char *name, void *value );
    
    // 获取对象实例变量的值, outValue: 返回时包含指向实例变量值的指针。
    Ivar object_getInstanceVariable ( id obj, const char *name, void **outValue );
    // 返回指向给定对象分配的任何额外字节的指针
    void * object_getIndexedIvars ( id obj );
    
    /**
      *读取对象中实例变量的值。
     *
      * @param obj包含要读取其值的实例变量的对象。
      * @param ivar Ivar描述要读取其值的实例变量。
      * @return : 由ivar指定的实例变量的值,如果object为nil,则为nil。
      * @note:如果已知实例变量的Ivar,则object_getIvar比object_getInstanceVariable快。
     */
    //如果使用ARC,则在获取`不是对象`的`返回值`时,执行项目会导致项目崩溃。但是如果`自己创建的类`,然后`自己添加的属性`,就可以用这个函数获取返回值(不管什么类型)
    
    // 返回对象中实例变量的值
    id object_getIvar ( id obj, Ivar ivar );
    
    // 设置对象中实例变量的值
    void object_setIvar ( id obj, Ivar ivar, id value );
    
    // 返回给定对象的类名
    const char * object_getClassName ( id obj );
    
    // 返回对象的类
    Class object_getClass ( id obj );
    /**
      *设置对象的类。
     *
      * @param obj要修改的对象。
      * @param cls一个类对象。
      * @return对象类的先前值,或如果对象为nil,则为Nil。
     */
    // 设置对象的类
    Class object_setClass(id  obj, Class  cls);
    
    
    • 如果实例变量的Ivar已经知道,那么调用object_getIvar会比object_getInstanceVariable函数,相同情况下,object_setIvar也比object_setInstanceVariable快。
    常见面试题

    1.runtime 怎么添加成员变量,属性,方法等?

    2.能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?

    参考答案:(自己总结的,若有误或更好的答案,请留言

    添加变量的方法
    • 不能向编译后得到的类中增加实例变量;
    • 能向运行时创建的类中添加实例变量;
    • 分析如下:
      • 因为编译后的类已经注册在runtime中,类结构体中的objc_ivar_list 实例变量的链表和instance_size实例变量的内存大小已经确定,同时runtime 会调用class_setIvarLayout 或 class_setWeakIvarLayout来处理strong weak引用,所以不能向存在的类中添加实例变量
      • 运行时创建的类可以添加实例变量,调用 class_addIvar函数,但是得在调用objc_allocateClassPair之后,objc_registerClassPair之前,原因同上。

    所谓静态语言,就是在程序运行前决定了所有的类型判断,类的所有成员方法编译阶段确定好了内存地址。也就意味着所有类对象只能访问属于自己成员变量方法,否则编译器直接报错。

    动态语言,恰恰相反,类型的判断类的成员变量、方法的内存地址都是在程序的运行阶段才最终确定,并且还能动态的添加成员变量和方法。也就意味着你调用一个不存在的方法时,编译也能通过,甚至一个对象它是什么类型并不是表面我们所看到的那样,只有运行之后才能决定其真正的类型。相比于静态语言,动态语言具有较高的灵活性和可订阅性。而oc,正是一门动态语言。

    添加属性 添加方法
    小结

    在这一章中我们介绍了Runtime运行时中与类和对象相关的数据结构,通过这些数据函数,我们可以管窥Objective-C底层面向对象实现的一些信息。另外,通过丰富的操作函数,可以灵活地对这些数据进行操作。

    参考资料:

    Objective-C Runtime Programming Guide
    Objective-CRuntime Reference
    http://southpeak.github.io/2014/10/25/objective-c-runtime-1/

    相关文章

      网友评论

          本文标题:Objective-C Runtime 运行时之一: 类与对象及

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