美文网首页
Class - 分析

Class - 分析

作者: reboot_q | 来源:发表于2018-12-20 20:42 被阅读9次

    Class 本质

    1. 结构代码

    // 被弃用的源码
    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;
    
    // 当前版本的源码
    struct objc_class : objc_object {
        Class superclass;
        cache_t cache;             // 方法缓存
        class_data_bits_t bits;  // 用于获取具体类信息 
    
        // 存储着方法列表, 属性列表, 协议列表
        class_rw_t *data() {
            reture bits.data();
        }
        void setData(class_rw_t *newData) {
            bits.setData(newData);
        }
    }
    
      class_rw_t *data() {
      return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    

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

    1.2 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*baseMethod() const {
            return baseMethodList;
        }
    }
    

    总结
    以方法列表为例,class_rw_t中的methods是二维数组的结构,并且可读可写,因此可以动态的添加方法,并且更加便于分类方法的添加。因为我们在Category的本质里面提到过,attachList函数内通过memmove 和 memcpy两个操作将分类的方法列表合并在本类的方法列表中。那么此时就将分类的方法和本类的方法统一整合到一起了。

    其实一开始类的方法,属性,成员变量属性协议等等都是存放在class_ro_t中的,当程序运行的时候,需要将分类中的列表跟类初始的列表合并在一起的时,就会将class_ro_t中的列表和分类中的列表合并起来存放在class_rw_t中,也就是说class_rw_t中有部分列表是从class_ro_t里面拿出来的。并且最终和分类的方法合并。可以通过源码提现这里一点。

    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));
    
        // 最开始cls->data是指向ro的
        ro = (const class_ro_t *)cls->data();
    
        if (ro->flags & RO_FUTURE) { 
            // rw已经初始化并且分配内存空间
            rw = cls->data();  // cls->data指向rw
            ro = cls->data()->ro;  // cls->data()->ro指向ro
            cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
        } else { 
            // 如果rw并不存在,则为rw分配空间
            rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1); // 分配空间
            rw->ro = ro;  // rw->ro重新指向ro
            rw->flags = RW_REALIZED|RW_REALIZING;
            // 将rw传入setData函数,等于cls->data()重新指向rw
            cls->setData(rw); 
        }
    }
    

    那么从上述源码中就可以发现,类的初始信息本来其实是存储在class_ro_t中的,并且ro本来是指向cls->data()的,也就是说bits.data()得到的是ro,但是在运行过程中创建了class_rw_t,并将cls->data指向rw,同时将初始信息ro赋值给rw中的ro。最后在通过setData(rw)设置data。那么此时bits.data()得到的就是rw,之后再去检查是否有分类,同时将分类的方法,属性,协议列表整合存储在class_rw_t的方法,属性及协议列表中。

    通过上述对源码的分析,我们对class_rw_t内存储方法、属性、协议列表的过程有了更清晰的认识,那么接下来探寻class_rw_t中是如何存储方法的。

    2. class_rw_t中是如何存储方法的

    2.1 method_t
    我们知道method_array_tproperty_array_tprotocol_array_t中以method_array_t为例,method_array_t中最终存储的是method_tmethod_t是对方法、函数的封装,每一个方法对象就是一个method_t。通过源码看一下method_t的结构体

    struct method_t {
        SEL name;  // 函数名
        const char *types;  // 编码(返回值类型,参数类型)
        IMP imp; // 指向函数的指针(函数地址)
    };
    

    2.2 SEL
    SEL代表方法名(选择器), 底层跟 char *类似 typedef struct objc_selector *SEL, 可以把SEL看做是方法名或者是字符串 @selector()sel_registerName().
    获取SEL字符串: sel_getName() NSStringFromSelector()

    2.3 types
    types包含了函数返回值, 参数编码的字符串. 通过字符串拼接的方式将返回值和参数拼接成一个字符串, 来表示函数返回值及参数. v16@0:8

    v         16       @        0        :       8
    void               id                  SEL
    // 16表示参数的占用空间大小, id后面的0 表示从0位开始存储, id占8位空间.
    // SEL后面的8表示从第8位开始存储, SEL同样占8位空间
    
    image.png

    2.4 IMP
    IMP 代表函数的具体实现, 存储的是函数的地址. 找到该地址, 也就找到了函数的实现, 进而进行函数调用.

    2.5 cache_t

    struct cache_t {
        struct bucket_t *_buckets; // 散列表 数组
        mask_t _mask;                 // 散列表的长度 -1
        mask_t _occupied;           // 已经缓存的方法数量
    };
    

    bucket_t是以散列表的方式存储方法列表的(哈希表).
    散列表(hash table) 是根据关键码值(key value)而直接进行访问的数据结构. 也就是说, 他通过把关键码值映射到列表中一个位置来访问记录, 以加快查找的速度. 这个映射函数叫散列函数, 存放记录的数组叫做散列表.

    struct bucket_t {
    private:
        cache_key_t _key;     // SEL作为Key
        IMP _imp;               // 函数的内存地址
    };
    

    散列表实现原理
    cache_fill cache_fill_nolock

    void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
    {
    #if !DEBUG_TASK_THREADS
        mutex_locker_t lock(cacheUpdateLock);
        cache_fill_nolock(cls, sel, imp, receiver);
    #else
        _collecting_in_critical();
        return;
    #endif
    }
    
    static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
    {
        cacheUpdateLock.assertLocked();
        // 如果没有initialize直接return
        if (!cls->isInitialized()) return;
        // 确保线程安全,没有其他线程添加缓存
        if (cache_getImp(cls, sel)) return;
        // 通过类对象获取到cache 
        cache_t *cache = getCache(cls);
        // 将SEL包装成Key
        cache_key_t key = getKey(sel);
       // 占用空间+1
        mask_t newOccupied = cache->occupied() + 1;
       // 获取缓存列表的缓存能力,能存储多少个键值对
        mask_t capacity = cache->capacity();
        if (cache->isConstantEmptyCache()) {
            // 如果为空的,则创建空间,这里创建的空间为4个。
            cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
        }
        else if (newOccupied <= capacity / 4 * 3) {
            // 如果所占用的空间占总数的3/4一下,则继续使用现在的空间
        }
        else {
           // 如果占用空间超过3/4则扩展空间
            cache->expand();
        }
        // 通过key查找合适的存储空间。
        bucket_t *bucket = cache->find(key, receiver);
        // 如果key==0则说明之前未存储过这个key,占用空间+1
        if (bucket->key() == 0) cache->incrementOccupied();
        // 存储key,imp 
        bucket->set(key, imp);
    }
    

    reallocate

    void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
    {
        // 旧的散列表能否被释放
        bool freeOld = canBeFreed();
        // 获取旧的散列表
        bucket_t *oldBuckets = buckets();
        // 通过新的空间需求量创建新的散列表
        bucket_t *newBuckets = allocateBuckets(newCapacity);
    
        assert(newCapacity > 0);
        assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
        // 设置Buckets和Mash,Mask的值为散列表长度-1
        setBucketsAndMask(newBuckets, newCapacity - 1);
        // 释放旧的散列表
        if (freeOld) {
            cache_collect_free(oldBuckets, oldCapacity);
            cache_collect(false);
        }
    }
    

    上述源码中首次传入reallocate函数的newCapacityINIT_CACHE_SIZEINIT_CACHE_SIZE是个枚举值,也就是4。因此散列表最初创建的空间就是4个。

    enum {
        INIT_CACHE_SIZE_LOG2 = 2,
        INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2)
    };
    

    expand()
    当散列表的空间被占用超过3/4的时候,散列表会调用expand ()函数进行扩展,我们来看一下expand ()函数内散列表如何进行扩展的。

    void cache_t::expand()
    {
        cacheUpdateLock.assertLocked();
        // 获取旧的散列表的存储空间
        uint32_t oldCapacity = capacity();
        // 将旧的散列表存储空间扩容至两倍
        uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
        // 为新的存储空间赋值
        if ((uint32_t)(mask_t)newCapacity != newCapacity) {
            newCapacity = oldCapacity;
        }
        // 调用reallocate函数,重新创建存储空间
        reallocate(oldCapacity, newCapacity);
    }
    

    find
    最后来看一下散列表中如何快速的通过key找到相应的bucket呢?我们来到find函数内部

    bucket_t * cache_t::find(cache_key_t k, id receiver)
    {
        assert(k != 0);
        // 获取散列表
        bucket_t *b = buckets();
        // 获取mask
        mask_t m = mask();
        // 通过key找到key在散列表中存储的下标
        mask_t begin = cache_hash(k, m);
        // 将下标赋值给i
        mask_t i = begin;
        // 如果下标i中存储的bucket的key==0说明当前没有存储相应的key,将b[i]返回出去进行存储
        // 如果下标i中存储的bucket的key==k,说明当前空间内已经存储了相应key,将b[i]返回出去进行存储
        do {
            if (b[i].key() == 0  ||  b[i].key() == k) {
                // 如果满足条件则直接reutrn出去
                return &b[i];
            }
        // 如果走到这里说明上面不满足,那么会往前移动一个空间重新进行判定,知道可以成功return为止
        } 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);
    }
    

    函数cache_hash (k, m)用来通过key找到方法在散列表中存储的下标,来到cache_hash (k, m)函数内部.

    static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
    {
        return (mask_t)(key & mask);
    }
    

    _mask
    _mask的值是散列表的长度减一,那么任何数通过与_mask进行按位与运算之后获得的值都会小于等于_mask,因此不会出现数组溢出的情况。

    总结

    当第一次使用方法时,消息机制通过isa找到方法之后,会对方法以SEL为keyIMP为value的方式缓存在cache_buckets中,当第一次存储的时候,会创建具有4个空间的散列表,并将_mask的值置为散列表的长度减一,之后通过SEL & mask计算出方法存储的下标值,并将方法存储在散列表中。举个例子,如果计算出下标值为3,那么就将方法直接存储在下标为3的空间中,前面的空间会留空。

    当散列表中存储的方法占据散列表长度超过3/4的时候,散列表会进行扩容操作,将创建一个新的散列表并且空间扩容至原来空间的两倍,并重置_mask的值,最后释放旧的散列表,此时再有方法要进行缓存的话,就需要重新通过SEL & mask计算出下标值之后在按照下标进行存储了。

    如果一个类中方法很多,其中很可能会出现多个方法的SEL & mask得到的值为同一个下标值,那么会调用cache_next函数往下标值-1位去进行存储,如果下标值-1位空间中有存储方法,并且key不与要存储的key相同,那么再到前面一位进行比较,直到找到一位空间没有存储方法或者key与要存储的key相同为止,如果到下标0的话就会到下标为_mask的空间也就是最大空间处进行比较。

    当要查找方法时,并不需要遍历散列表,同样通过SEL & mask计算出下标值,直接去下标值的空间取值即可,同上,如果下标值中存储的key与要查找的key不相同,就去前面一位查找。这样虽然占用了少量控件,但是大大节省了时间,也就是说其实apple是使用空间换取了存取的时间。

    相关文章

      网友评论

          本文标题:Class - 分析

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