Runtime——methods成员变量,cache成员变量

类,元类的methods成员变量

在之前分析类的时候,类里面的methods存储着该类所有的实例方法信息,那么它具体是怎么存储的?

class method_array_t : 
    public list_array_tt<method_t, method_list_t> 
{
    
    
    typedef list_array_tt<method_t, method_list_t> Super;

 public:
    method_list_t **beginCategoryMethodLists() {
    
    
        return beginLists();
    }
    
    method_list_t **endCategoryMethodLists(Class cls);

    method_array_t duplicate() {
    
    
        return Super::duplicate<method_array_t>();
    }
};
// MARK: - method结构声明
struct method_t {
    
    
    SEL name;//SEL
    const char *types;//方法参数和类型
    MethodListIMP imp;//imp

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
    
    
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        {
    
     return lhs.name < rhs.name; }
    };
};

查看源码发现其实methods是一个数组指针,这个数组的大小为(N + 1)* 8个字节(N为分类个数),也就是说这个数组里面存着一些一维数组,指向真正的实例方法列表,也就是分类1的实例方法列表,类本身的方法列表等等。方法列表里面放着一个个实例方法method_t,看看他的定义。

// MARK: - method结构声明
struct method_t {
    
    
    SEL name;//SEL
    const char *types;//方法参数和类型
    MethodListIMP imp;//imp

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
    
    
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        {
    
     return lhs.name < rhs.name; }
    };
};
typedef struct method_t *Method; // 方法声明
// 方法的本质就是一个method_t结构体指针,它可以指向任何一个方法。

可以发现里面就三个指针,所以它只占用了24个字节,这些内存都是在静态区的。元类的methods同理,只不过保存的是类方法列表。
下面分析一下这三个成员变量。

  • SEL方法选择器,跟方法名一一对应,是一个方法的唯一标识,可以当作方法名看待,之前使用的@selector(方法名)就是获得一个方法选择器。
  • types类型编码字符串,包含了方法的参数和返回值信息,编码中第一个值代表返回值类型,后面字母依次表示该方法的各个参数类型。第一个数字代表所有参数占用总内存,后面的数字代表各参数内存地址的偏移量。
  • IMP函数指针,存储着一个地址,指向该方法在代码区的具体实现。

类,元类的cache成员变量

当一个对象接受到消息时,会根据它的isa指针找到它所属的类,然后根据类的methods找到所有的方法列表,然后依次遍历这些方法列表来查找要执行的方法。在实际情况中,一个对象只有一部分方法是常用的,其余方法用的频率很低,如果对象每接受一次消息就要遍历一次所有的列表,性能肯定很差。
类的cache成员变量就是用来解决这个问题的。
系统每次调用一次方法,就会将这个方法存储到cache中,下次再调用方法就优先从cache中查找。找不到再去methods里面找,大大提高了方法查找的效率。
看看cache源码。

struct cache_t {
    
    
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
}
// MARK: - bucket_t声明结构
struct bucket_t {
    
    
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
    MethodCacheIMP _imp;
    cache_key_t _key;
#else
    cache_key_t _key;
    MethodCacheIMP _imp;
#endif

public:
    inline cache_key_t key() const {
    
     return _key; }
    inline IMP imp() const {
    
     return (IMP)_imp; }
    inline void setKey(cache_key_t newKey) {
    
     _key = newKey; }
    inline void setImp(IMP newImp) {
    
     _imp = newImp; }

    void set(cache_key_t newKey, IMP newImp);
};

分析一下成员变量

  • _buckets:方法缓存散列表
  • _mask:散列表长度 - 1
  • _occupied:缓存方法数量

可以看出散列表里面的元素不直接是method_t,而是bucket_t,点开它的结构可以发现,它有两个成员变量IMP和cache_key_t.IMP就是一个函数指针,令一个忙猜是函数标识。
下面看看apple如何实现这个散列表。

// Class points to cache. SEL is key. Cache buckets store SEL+IMP.
// Caches are never built in the dyld shared cache.
// 可以在这里的sel is key看出cache_key_t等同于唯一标识SEL,cahe散列表存储了IMP和SEL
// 缓存不会构建在dyld共享缓存中
static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
{
    
    
    // 这里的散列算法很简单,就是用key&长度-1获得index
    // SEL是方法唯一标识
    return (mask_t)(key & mask);
}

cache_hash冲突

在实际过程中,有可能遇见这样的问题,不同的sel & n - 1后得到了相同的index,那么会产生数据冲突。这时如何处理?

// 散列表读取
bucket_t * cache_t::find(cache_key_t k, id receiver)
{
    
    
    assert(k != 0);
    // 获取散列表和长度-1
    bucket_t *b = buckets();
    mask_t m = mask();
    // 通过散列算法得到某个长度的索引
    mask_t begin = cache_hash(k, m);
    mask_t i = begin;
    do {
    
    
        // 读取index的元素对比SEL,判断是否和我们需要的相等,返回
        // 或者找到空闲内存,说明第一次调用,存入
        if (b[i].key() == 0  ||  b[i].key() == k) {
    
    
            return &b[i];
        }
    } while ((i = cache_next(i, m)) != begin);
    // 否则Index - 1,遍历散列表,直到读取到想要的SEL
    // hack
    
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    // 判断一些错误情况
    cache_t::bad_cache(receiver, (SEL)k, cls);
}

可以看见这里需要对比index处是否空闲或者元素SEL是否和我们搜索的相等,如果没找到我们就会index - 1,遍历散列表,直到找到空闲的内存,或者真正的方法。
读取我们直接根据index拿方法,不需要遍历。

cache散列表扩容

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
    
    
    bool freeOld = canBeFreed();
    
    bucket_t *oldBuckets = buckets();
    // 开辟新的散列表
    bucket_t *newBuckets = allocateBuckets(newCapacity);

    // Cache's old contents are not propagated. 
    // This is thought to save cache memory at the cost of extra cache fills.
    // fixme re-measure this

    assert(newCapacity > 0);
    assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);

    setBucketsAndMask(newBuckets, newCapacity - 1);
    
    if (freeOld) {
    
    
        // 释放旧的散列表,清空其缓存
        cache_collect_free(oldBuckets, oldCapacity);
        cache_collect(false);
    }
}

当散列表的内存不够用时,系统进行两倍扩容。
本文参考——参考文章

猜你喜欢

转载自blog.csdn.net/chabuduoxs/article/details/125419124