美文网首页
iOS-浅谈OC中的Class对象

iOS-浅谈OC中的Class对象

作者: 晴天ccc | 来源:发表于2019-05-16 09:12 被阅读0次

    目录

    • Class对象
      ----class的结构
      ----class_rw_t的结构
      ----class_ro_t的结构结
      ---metho_t的结构
      ---Type Encoding
    • cache_t方法缓存
      ----方法调用的问题思考
      ----cache_t结构
      ----向缓存中存入方法
      ----从缓存中查找方法
      ----补充:散列表索引号计算
      ----实战拓展+方法/缓存方法调用过程解析
      ----方法查找过程
    • Class的结构

    下面我们就通过查看objc源码,发现class对象底层为objc_class结构体。最终的Class的数据底层结构如下图:

    在前面文章提到,OC对象有三种数据类型,里面就包含了Class对象。

    下面是具体分析

    struct objc_class : objc_object {
        // Class ISA;              // 继承来的isa指针
        Class superclass;          // 父类指针
        cache_t cache;             // 方法缓存 formerly cache pointer and vtable
        class_data_bits_t bits;    // 获取具体的类信息 class_rw_t * plus custom rr/alloc flags
        
        class_rw_t *data() const {
            return bits.data();
        }
        // .... more
        // .... more 
        // .... more 代码经过精简展示
    }
    
    

    从中我们看到Class结构体中包含了isa共用体superclass指针方法缓存cachebits

    struct class_data_bits_t {
        friend objc_class;
    
        // Values are the FAST_ flags above.
        uintptr_t bits;
    private:
        bool getBit(uintptr_t bit) const
        {
            return bits & bit;
        }
    
        class_rw_t* data() const {
            return (class_rw_t *)(bits & FAST_DATA_MASK);
        }
        // .... more 代码经过精简展示
    };
    

    由位运算知识点可知,bits保存了Class的一些信息,通过bits & FAST_DATA_MASK可以获取类的class_rw_t信息。

    struct class_rw_t {
        // Be warned that Symbolication knows the layout of this structure.
        uint32_t flags;
        uint16_t witness;
    #if SUPPORT_INDEXED_ISA
        uint16_t index;
    #endif
        
        Class firstSubclass;
        Class nextSiblingClass;
    
        const class_ro_t *ro() const { }    // 里面内容暂时省略
        const method_array_t methods() const { }    // 里面内容暂时省略
        const property_array_t properties() const { }    // 里面内容暂时省略
        const protocol_array_t protocols() const { }    // 里面内容暂时省略
        // .... more 代码经过精简展示
    };
    

    class_rw_t包含了方法列表、属性列表、协议列表等信息。
    还包含了一个class_ro_t结构体,里面保存了class的类名、成员变量等初始信息。

    struct class_ro_t {
        uint32_t flags;
        uint32_t instanceStart;
        uint32_t instanceSize;
    #ifdef __LP64__
        uint32_t reserved;
    #endif
        union {
            const uint8_t * ivarLayout;
            Class nonMetaclass;
        };
        explicit_atomic<const char *> name;
        void *baseMethodList;
        protocol_list_t * baseProtocols;
        const ivar_list_t * ivars;
        const uint8_t * weakIvarLayout;
        property_list_t *baseProperties;
        // .... more 代码经过精简展示
    };
    
    • class_rw_t的结构

    实际上class_rw_t内部保存的是method_array_tproperty_array_tprotocol_array_t类型。和method_list_t*意义相同,是二维数组。

    • class_rw_t内部的methods、properties、protocols都是二维数组,是可读可写的,包含了类的初始内容、分类的内容。
    • 比如methods内部保存了method_list_t对象>method_list_t内部保存了真正的方法_method_t,类初始方法列列表会排在最后,后编译的分类中的方法会保存在methods前面。
    • class_ro_t的结构

    class_ro_t里面的baseMethodList、baseProtocols、ivars、baseProperties是一维数组,是只读的,包含了类的初始内容。

    • metho_t的结构

    struct method_t {
        SEL name; // 函数名
        const char *types; // 编码(返回值类型、参数类型)
        IMP imp; //指向函数的指针()
    };
    
    • IMP代表函数的具体实现
    • types包含了函数返回值、参数编码的字符串。
    • SEL代表方法\函数名,一般叫做选择器,底层结构跟char *类似
      可以通过@selector()和sel_registerName()获得。
      可以通过sel_getName()和NSStringFromSelector()转成字符串。
      不同类中相同名字的方法,所对应的方法选择器是相同的。
    • Type Encoding

    iOS中提供了一个叫做@encode的指令,可以将具体的类型表示成字符串编码,可以通过打印查看:

        NSLog(@"@encode(int) = %s", @encode(int));
        NSLog(@"@encode(void) = %s", @encode(void));
        NSLog(@"@encode(id) = %s", @encode(id));
        NSLog(@"@encode(SEL) = %s", @encode(SEL));
    
        @encode(int) = i
        @encode(void) = v
        @encode(id) = @
        @encode(SEL) = :
    

    比如下面两个方法,test方法对应metho_t中的types值就是v16@0:8testAge:height:方法对应metho_t中的types值就是v24@0:8i16i20

    // v16@0:8
    - (void)test;
    
    // v24@0:8i16i20
    - (void)testAge:(int)age height:(int)height;
    

    test方法默认会带有两个参数:

    - (void)testSelf:(id)self _cmd:(SEL)_cmd;
    

    也可以简写成v@:

    方法缓存cache_t

    • 方法调用的问题思考

    Person *per =  [[Person alloc] init ];
    [per run];
    [per run];
    
    • instance对象的方法在类对象中存储着。class对象的方法在元类对象中存储着。
    • instance对象调用对象方法的轨迹:
      isa找到class,方法不存在,就通过superclass找父类。
    • class对象调用类方法的轨迹:
      isa找到meta-class,方法不存在,就通过superclass找父类的meta-class,
      如果基类中也不存在,则去基类的class对象中查找同名对象方法。

    如果一直调用run方法,每次按照这种调用,效率是很低的。所以设计了一种cache结构来优化性能。

    • cache_t结构

    cache_t用散列表来缓存曾经调用过的方法,可以提高方法的查找速度,避免多次执行方法查找逻辑。

    散列表(Hash table,也叫哈希表):是根据关键码值(Key value)而直接进行访问的数据结构。
    它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

    代码如下:

    struct cache_t {
        bucket_t *_buckets; // 散列表
        mask_t _mask; // 散列长度-1
        mask_t _occupied; // 已缓存的方法数量
    };
    
    struct bucket_t {
        cache_key_t _key; // SEL作为key
        IMP _imp; // 函数内存地址
    };
    
    • 例如散列表_buckets初始长度是10,那么_mask是9,_occupied可能是3。
    • 向缓存中存入方法

    按照上面思考的逻辑,第一次执行run方法的时候,系统会把run方法缓存到_buckets数组中,类型是bucket_t

    key   =  @selector(run)
    _img  =  run的地址
    
    • 在查找方法时,通过sel(方法名)作为key,找到对应的bucket_t,从而找到_imp(实现)进行调用。
    • 补充:SEL = @selector(run)
    • 从缓存中查找方法

    • _buckets散列表(哈希)索引计算方法:sel & mask
    • 比如调用了方法@selector(run),需要先【计算出散列表索引】,再将bucket_t插入到索引位置。
    • _buckets其他位置如果没有信息会留空,是以空间换时间的方案。

    举例:@selector(run) & _mask = 6
    如果多个方法,同理在相应序号插入方法即可:@selector(eat) & _mask = 2则在序号2插入该方法即可。

    • 补充:散列表索引号计算

    static inline mask_t cache_hash(SEL sel, mask_t mask) 
    {
        uintptr_t value = (uintptr_t)sel;
    #if CONFIG_USE_PREOPT_CACHES
        value ^= value >> 7;
    #endif
        return (mask_t)(value & mask);
    }
    

    如果不同的方法通计算出来的索引值相同,就需要解决哈希冲突,苹果的方法时让索引-1:

    static inline mask_t cache_next(mask_t i, mask_t mask) {
        return i ? i-1 : mask;
    }
    
    • 写入:计算出来的索引位置已经保存了bucket_t,则让索引-1,如果还不行继续-1,索引小于0时,将索引设置成mask(_buckets长度-1)从最后的位置继续向前遍历,直到找到空位置,进行插入操作。
    • 读取:计算出来的索引位置对应的bucket_t中的SEL如果和传入的SEL不一致,就让索引-1查找,如果还不相同,则继续-1,索引小于0时将索引设置成mask(_buckets长度-1),从最后位置继续向前遍历,直到找到SEL相同的bucket_t。
    • 扩容:当_buckets空间不够时会进行扩容操作(原有空间大小*2),会更新mask的值,并且清空_buckets。
    • 实战拓展+方法/缓存方法调用过程解析

    我们创建三个类:Person、Student、GoodStudent继承关系如下代码所示,分别实现方法:run、studentRun、goodStudentRun

    #import <Foundation/Foundation.h>
    @interface Person : NSObject
    - (void)run;
    @end
    
    #import "Person.h"
    @interface Student : Person
    - (void)studentRun;
    @end
    
    #import "Student.h"
    @interface GoodStudent : Student
    - (void)goodStudentRun;
    @end
    
    
    Person *per =  [[Person alloc] init ];
    [per run];
    [per run];
    

    方法/缓存方法调用过程:

    • 第一次调用run方法的时候,通过instance对象perisa指针找到Person类对象
    • 先在cache这个散列表中查找有无该方法缓存,通过sel(方法名)作为key,找到对应的bucket_t,结果肯定空的的,该方法不存在。
    • 然后通过bitsclass_rw_tproperties方法列表中查找run方法,结果找到了,进行方法执行。
    • 然后把相关信息规整成bucket_t,存入Person类对象cache散列表中。
    • 第二次调用run方法的时候,直接在Person类对象cache这个散列表中找到缓存方法并调用即可。
    Student *stu =  [[Student alloc] init ];
    [stu run];
    [stu run];
    

    子类调用父类方法的过程:

    • 调用过程和上面类似,instance对象stu在自己class方法也就是Student对象中查找有无run方法,结果是没有的。
    • 通过superclass指针去父类中进行查找,顺序是先查父类的cache,再查父类的properties
    • 因为上面[per run];执行过,所以在父类的cache中有缓存。
    • 在父类的cache中查找到run方法,直接调用执行。
    • 查然后在自己class对象的cache中也会缓存一份。
    • 当再此执行[stu run];的时候,直接在自己的class对象的cache中查找调用即可。
    • 方法查找过程

    isasuperclass中说明了OC方法的调用轨迹,这里做一下补充,在查找过程中需要先查找方法缓存,过程如下:

    相关文章

      网友评论

          本文标题:iOS-浅谈OC中的Class对象

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