Runtime笔记(三)—— OC Class的方法缓存cache_t

本文详细介绍了Objective-C中Class的方法缓存机制`cache_t`,阐述了方法缓存的作用、数据结构及缓存过程,包括方法的存入、查询、扩容操作。当方法被频繁调用时,`cache_t`能显著提升查找效率,避免遍历方法列表。文中还探讨了父类方法缓存的情况以及消息发送流程。
摘要由CSDN通过智能技术生成

Runtime系列文章

Runtime笔记(一)—— isa的深入体会(苹果对isa的优化)
Runtime笔记(二)—— Class结构的深入分析
Runtime笔记(三)—— OC Class的方法缓存cache_t
Runtime笔记(四)—— 刨根问底消息机制
Runtime笔记(五)—— super的本质
[Runtime笔记(六)—— Runtime的应用…待续]-()
[Runtime笔记(七)—— Runtime的API…待续]-()
Runtime笔记(八)—— 记一道变态的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  用于获取具体的类信息

};

这里面还有一个cache_t cache没有解读过,一起来看一看这个东西。看名字很好理解,就是缓存的意思,缓存什么呢?——缓存方法。
它的底层是通过散列表(哈希表)的数据结构来实现的,用于缓存曾经调用过的方法,可以提高方法的查找速度。
首先,回顾一下正常情况下方法调用的流程。假设我们调用一个实例方法[bj XXXX];

  • obj -> isa -> objClass对象 -> method_array_t methods -> 对该表进行遍历查找,找到就调用,没找到继续往下走
  • obj -> superclass -> obj的父类 -> isa -> method_array_t methods -> 对父类的方法列表进行遍历查找,找到就调用,没找到就重复本步骤
  • 找到就调用,没找到重复流程
  • 找到就调用,没找到重复流程
  • 找到就调用,没找到重复流程
  • 直到NSObject -> isa -> NSObjectClass对象 -> method_array_t methods …

如果XXXX方法在程序内会被频繁的调用,那么这种逐层便利查找的方式肯定是效率低下的,因此苹果设计了cache_t cache,当XXXX第一次被调用的时候,会按照常规流程查找,找到之后,就会被加入到cache_t cache中,当再次被调用的时候,系统就会直接现到cache_t cache来查找,找到就直接调用,这样便大大提升了查找的效率。

刚才介绍了cache_t cache是通过散列表来实现的,下面就来着重分析一下,方法是如何被缓存的。散列/哈希表,想必大部分iOS开发者至少应该听过,而我们常用的NSDictionary其实就是一种散列表数据结构。来看一下cache_t cache的定义

struct cache_t {
   
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
}
  • struct bucket_t *_buckets; —— 用来缓存方法的散列/哈希表
  • mask_t _mask; —— 这个值 = 散列表长度 - 1
  • mask_t _occupied; —— 表示已经缓存的方法的数量

上面介绍的_buckets散列表里面的存储单元是bucket_t,来看看它包含了方法的什么信息

struct bucket_t {
   
private:
    cache_key_t _key;
    IMP _imp;
}
  • cache_key_t _key; —— 这个key实际上就是方法的SEL,也就是方法名
  • IMP _imp; —— 这个就是方法对应的函数的内存地址

想一想我们平时是怎么使用NSDictionary的,通过一堆Key-Value键值对来进行存储的,NSDictionary的底层就是散列表,这个刚才说过。方法缓存的时候,key就是上面的cache_key_t _key;,value就是上面的bucket_t结构体对象。

但是散列表的运作原理到底如何呢,这个属于数据结构问题,这里简要介绍一下。首先散列表本质上就是一个数组
在往散列表里面添加成员的时候,首先需要借助key计算出一个index,然后再将元素插入散列表的index位置
往散列表插值
那么从散列表里面取值就显而易见了,根据一个key,计算出index,然后到散列表对应位置将值取出根据key从散列表取值

这里的查询方法的时候(也就是取值操作),时间复杂度为O(1), 对比我们一开始从方法列表的遍历查询所对应的时间复杂度为O(n),因此通过缓存方法,可以极大的提高方法查询的效率,从而提高了方法调用机制的效率。

根据key计算出index值的这个算法称作散列算法,这个算法可以由你自己设计,总之目的就是尽可能减少不同的key得出相同index的情况出现,这种情况被称作哈希碰撞,同时还要保证得出的index值在合理的范围。index越大,意味着对应的散列表的长度越长,这是需要占用实际物理空间的,而我们的内存是有限的。散列表是一种通过牺牲一定空间,来换取时间效率的设计思想。

我们通过key计算出的index大小是随机的,无顺序的,因此在方法缓存的过程中,插入的顺序也是无顺序的
而且可以预见的是,散列表里面再实际使用中会有很多位置是空着的,比如散列表长度为16,最终值存储了10个方法,散列表长度为64,最终可能只会存储40个方法,有一部分空间终究是要被浪费的。但是却提高查找的效率。这既是所谓的空间换时间。

再介绍一下苹果这里所采用的散列算法,其实很简单,如下
index = @selector(XXXX) & mask 根据&运算的特点,可以得知最终index <= mask,而mask = 散列表长度 - 1,也就是说 0 <= index <= 散列表长度 - 1,这实际上覆盖了散列表的索引范围。而刚刚我们还提到过一个问题——哈希碰撞,也就是不同的key得到相同的index,该怎么处理呢?我们看一下源码,在objc源码里面搜索cache_t,可以发现一个跟查找相关的方法

bucket_t * cache_t::find(cache_key_t k, id receiver)  //根据key值 k 进行查找
{
   
    assert(k != 0);

    bucket_t *b = buckets();
    mask_t m = mask();
    mask_t begin = cache_hash(k, m);   //通过cache_hash函数【begin  = k & m】计算出key值 k 对应的 index值 begin,用来记录查询起始索引
    mask_t i = begin; // begin 赋值给 i,用于切换索引
    do {
   
        if (b[i].key() == 0  ||  b[i].key() == k) {
    
              //用这个i从散列表取值,如果取出来的bucket_t的 key = k,则查询成功,返回该bucket_t,
              //如果key = 0,说明在索引i的位置上还没有缓存过方法,同样需要返回该bucket_t,用于中止缓存查询。
            return &b[i];
        }
    } while ((i = cache_next(i, m)) != begin);
// 这一步其实相当于 i = i-1,回到上面do循环里面,相当于查找散列表上一个单元格里面的元素,再次进行key值 k的比较,
//当i=0时,也就i指向散列表最首个元素索引的时候重新将mask赋值给i,使其指向散列表最后一个元素,重新开始反向遍历散列表,
//其实就相当于绕圈,把散列表头尾连起来,不就是一个圈嘛,从begin值开始,递减索引值,当走过一圈之后,必然会重新回到begin值,
//如果此时还没有找到key对应的bucket_t,或者是空的bucket_t,则循环结束,说明查找失败,调用bad_cache方法。

    // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)k, cls);
}

*********************************** cache_hash(k, m);
static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
{
   
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值