美文网首页iOS点点滴滴
Class的结构及方法缓存

Class的结构及方法缓存

作者: YY_Lee | 来源:发表于2019-02-21 15:01 被阅读0次

    从runtime源码中看到Class的结构如下

    struct objc_class : objc_object {
        // Class 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() { 
            return bits.data();
        }
        ......
    }
    
    // bits.data();
    class_rw_t* data() {
       return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    

    分别解释下几个字段

    • superclass:指向父类的指针
    • cache:调用过的方法缓存
    • bits:用于获取具体的类信息
    • class_rw_t:类具体信息的结构体,可以看到是由bits & FAST_DATA_MASK得到

    接着看看class_rw_t包含哪些信息,class_rw_t结构如下:

    struct class_rw_t {
        uint32_t flags;
        uint32_t version;
        const class_ro_t *ro;        //类的初始信息
        method_array_t methods;      //方法列表
        property_array_t properties; //属性列表
        protocol_array_t protocols;  // 协议列表
        Class firstSubclass;
        Class nextSiblingClass;
        char *demangledName;
        ......
    };
    

    class_rw_t中包含了方法、属性、协议等,还有个ro,这个ro指向一个class_ro_t对象,class_ro_t里面包含了类初始的信息,是只读的。

    class_ro_t的结构如下:

    struct class_ro_t {
        uint32_t flags;
        uint32_t instanceStart;
        uint32_t instanceSize; //instance对象占用的内存空间大小
    #ifdef __LP64__
        uint32_t reserved;
    #endif
        const uint8_t * ivarLayout;
        const char * name;              //类名
        method_list_t * baseMethodList; //初始方法列表
        protocol_list_t * baseProtocols; //初始协议列表
        const ivar_list_t * ivars;       // 成员变量列表
        const uint8_t * weakIvarLayout;
        property_list_t *baseProperties; //初始协议列表
        method_list_t *baseMethods() const {
            return baseMethodList;
        }
    };
    

    class_rw_t中methods、properties、protocols是二维数组,是可读可写的,包含类初始以及分类的方法、属性、协议。最开始是没有class_rw_t,class_rw_t是在运行时创建的,并将class_ro_t的内容和分类的内容添加进来;

    上面的结论可以从下面runtime的源码中看出,删减了部分代码,只保留了上述流程:

    /***********************************************************************
    * realizeClass
    * Performs first-time initialization on class cls, 
    * including allocating its read-write data.
    * Returns the real class structure for the class. 
    * Locking: runtimeLock must be write-locked by the caller
    **********************************************************************/
    static Class realizeClass(Class cls)
    {
        runtimeLock.assertWriting();
    
        const class_ro_t *ro;
        class_rw_t *rw;
        Class supercls;
        Class metacls;
        bool isMeta;
    
        if (!cls) return nil;
        if (cls->isRealized()) return cls;
        assert(cls == remapClass(cls));
        ro = (const class_ro_t *)cls->data(); //开始时bits是指向ro的
        if (ro->flags & RO_FUTURE) { 
            // This was a future class. rw data is already allocated.
            rw = cls->data();
            ro = cls->data()->ro;
            cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
        } else {
            // Normal class. Allocate writeable class data.
            rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
            rw->ro = ro;
            rw->flags = RW_REALIZED|RW_REALIZING;
            cls->setData(rw); // 创建完rw将ro赋值给rw的ro,并将rw赋值给cls的bits
        }
        // Attach categories
        methodizeClass(cls);
    
        return cls;
    }
    

    从上面源码注释可以看出这个函数是类第一次初始化时执行,最初是没有rw的,class的bits是指向ro的。rw创建完将ro赋值给rw的ro,并将rw赋值给cls的bits,最后从注释可以看出是处理分类的内容;

    函数methodizeClass源码如下:

    static void methodizeClass(Class cls)
    {
        runtimeLock.assertWriting();
    
        bool isMeta = cls->isMetaClass();
        auto rw = cls->data();
        auto ro = rw->ro;
    
        // Methodizing for the first time
        if (PrintConnecting) {
            _objc_inform("CLASS: methodizing class '%s' %s", 
                         cls->nameForLogging(), isMeta ? "(meta)" : "");
        }
    
        // Install methods and properties that the class implements itself.
        method_list_t *list = ro->baseMethods();
        if (list) {
            prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
            rw->methods.attachLists(&list, 1);
        }
    
        property_list_t *proplist = ro->baseProperties;
        if (proplist) {
            rw->properties.attachLists(&proplist, 1);
        }
    
        protocol_list_t *protolist = ro->baseProtocols;
        if (protolist) {
            rw->protocols.attachLists(&protolist, 1);
        }
    
        // Root classes get bonus method implementations if they don't have 
        // them already. These apply before category replacements.
        if (cls->isRootMetaclass()) {
            // root metaclass
            addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
        }
    
        // Attach categories.
        category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
        attachCategories(cls, cats, false /*don't flush caches*/);
    
    }
    

    从上述源码中可以看出,从rw找到ro取出里面的baseMethods、baseProperties、baseProtocols,添加到rw对应的methods、properties、protocols中。最后取出未添加过的分类内容添加进来。关于怎么将分类方法添加进来的,即attachCategories函数的具体实现,请查看About Category

    方法的缓存

    方法的调用如果每次都去类、父类、元类中一层层查找效率是比较低,所以runtime中对调用过的方法进行了缓存,放在类的cache中;

    讲方法缓存前先来了解下方法的底层结构:

    struct method_t {
        SEL name;          //函数名
        const char *types; //编码(返回值类型、参数类型)
        IMP imp;           //指向函数的指针(函数地址)
    };
    
    • imp:函数的具体实现
    typedef id (*IMP)(id, SEL, ...); 
    
    • SEL:代表方法\函数名,也叫方法选址器,底层结构跟char *类似;
    • types:包含返回值和参数编码的字符串
      iOS提供了@encode的指令,可将具体的类型表示成字符串编码,见下表(部分code)
    code Meaning
    c A char
    i An int
    s A short
    l A long
    q A long long
    c An unsigned char
    I An unsigned int
    S An unsigned short
    L An unsigned long
    Q An unsigned long long
    f A float
    d A double
    B A C++ bool or a C99 _Bool
    V A void
    * A charactor string(char *)
    @ An object(whether statically typed or typed id)
    : A method selector(SEL)
    ^type A pointer to type

    举个例子:types = "i20@0:8i16",OC中方法默认会传入id类型的self和SEL ,代表的含义如下

    返回值类型 参数总长度 参数1类型及开始位置 参数2类型及开始位置 参数3 类型及开始位置
    int类型 20 id 类型 SEL int类型

    先来看下缓存cache_t的结构:

    struct cache_t {
        struct bucket_t *_buckets; //哈希表,存储调用方法
        mask_t _mask; // 哈希表长度 - 1
        mask_t _occupied; //已经缓存的数量
    }
    struct bucket_t {
        cache_key_t _key;  // SEL作为key
        IMP _imp; //函数的内存地址
    }
    

    Class的方法缓存(cache_t)是用哈希表实现的,可以提高方法的查找效率

    散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表
    给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。

    一般哈希表是通过目标key &或者%上一个值,得到一个哈希表位置的下标;类方法缓存是用方法SEL作为key & _mask得到一个位置下标,然后将方法地址存入哈希表;哈希表下标从0开始,最大为哈希表长度减一,这也是_mask大小为哈希表长度减一的原因;

    下面是方法缓存函数的源码:

    static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
    {
        cacheUpdateLock.assertLocked();
        // Never cache before +initialize is done
        if (!cls->isInitialized()) return;
    
        // Make sure the entry wasn't added to the cache by some other thread 
        // before we grabbed the cacheUpdateLock.
        if (cache_getImp(cls, sel)) return;
        cache_t *cache = getCache(cls);
        cache_key_t key = getKey(sel);
    
        // Use the cache as-is if it is less than 3/4 full
        mask_t newOccupied = cache->occupied() + 1;
        mask_t capacity = cache->capacity();
        if (cache->isConstantEmptyCache()) {
            // Cache is read-only. Replace it.
            cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
        }
        else if (newOccupied <= capacity / 4 * 3) {
            // Cache is less than 3/4 full. Use it as-is.
            // 说明当缓存达到散列表的3/4时就会扩容
        }
        else {
            // Cache is too full. Expand it.
            cache->expand();
        }
        // Scan for the first unused slot and insert there.
        // There is guaranteed to be an empty slot because the 
        // minimum size is 4 and we resized at 3/4 full.
        //找到第一个可用的位置,插入。表最小长度为4并且在达到3/4容量时扩容,确保表一定有可用的位置
        bucket_t *bucket = cache->find(key, receiver);
        if (bucket->key() == 0) cache->incrementOccupied();// 表中没存过该方法_occupied自增
        bucket->set(key, imp);
    }
    

    从上述源码中可以看出散列表最小长度为4并且在达到3/4容量时扩容,确保表一定有可以插入的位置。插入前,查找表中是否已经存储过该方法,没存过_occupied加一。

    现在看下查找缓存方法的函数:

    bucket_t * cache_t::find(cache_key_t k, id receiver)
    {
        assert(k != 0);
    
        bucket_t *b = buckets();
        mask_t m = mask();
        mask_t begin = cache_hash(k, m); //key & mask得到的下标
        mask_t i = begin;
        do {
            if (b[i].key() == 0  ||  b[i].key() == k) {
                return &b[i];
            }
        } while ((i = cache_next(i, m)) != begin);
    
        // hack
        Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
        cache_t::bad_cache(receiver, (SEL)k, cls);
    }
    
    // arm64架构下的cache_next
    static inline mask_t cache_next(mask_t i, mask_t mask) {
        return i ? i-1 : mask;
    }
    

    因为不同key & mask得到的下标可能相同,所以通过key & mask得到初始下标后,拿到散列表下标对于的bucket对象取出key与目标key比较,相等表示就是我们想要的bucket。不同就循环查看i - 1的位置,当i=0时查看mask位置的bucket是否是想要的;如果遍历完之后还没找到正确的,做一些错误处理,具体查找cache_t::bad_cache函数查看。

    相关文章

      网友评论

        本文标题:Class的结构及方法缓存

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