当前位置: 首页 > news >正文

「OC」源码学习——cache_t的原理探究

「OC」源码学习——cache_t的原理探究

前言

上一次我们讲到了objc_class的isa和bits之中的内容,接下来就是cache_t之中的内容。

cache_t探究

前面我们提到在类对象之中isasuperclass各占8个字节,所以我们可以直接通过内存平移获取相关内容,于是我们打开lldb继续调试

image-20250504114456081

好吧,看起来好像一点有用的看不到,没关系,我们接着查找到cache_t的定义

image-20250504114805185

可以看到在当前架构下,有用的属性为 _bucketsAndMaybeMask_unused_flags_occupied_originalPreoptCache,但由于_originalPreoptCache_unused_flags_occupied组成的结构体组成了联合体,由于联合体的互斥特性,我们可以先不看_originalPreoptCache_unused是占位符可以实现八位对齐

image-20250504123227932

通过源码的学习,我们可以知道这时候其实可以通过cache提供给的buckets()方法去获取_buckets,就可以获取sel-imp了,这两个的获取在bucket_t结构体中同样提供了相应的获取方法sel() 以及 imp(pClass),我们可以通过内存偏移的方法去获取第二个

image-20250504135605122

img

总结出cache_t的结构如上图

insert函数

image-20250504123246615

在运行完-[GGObject speak]方法之后,_occupied进行了自增,而调用-[GGObject sayHello]之后在查看发现_occupied又变成了1,这是为什么呢?

我们来看看cache_t之中关于insert的函数,就是将函数缓存插入bucket_t之中的实现

void cache_t::insert(SEL sel, IMP imp, id receiver)
{lockdebug::assert_locked(&runtimeLock);// Never cache before +initialize is doneif (slowpath(!cls()->isInitialized())) {return;}if (isConstantOptimizedCache()) {_objc_fatal("cache_t::insert() called with a preoptimized cache for %s",cls()->nameForLogging());}#if DEBUG_TASK_THREADSreturn _collecting_in_critical();
#else
#if CONFIG_USE_CACHE_LOCKmutex_locker_t lock(cacheUpdateLock);
#endifASSERT(sel != 0 && cls()->isInitialized());// 缓存的容量管理mask_t newOccupied = occupied() + 1;unsigned oldCapacity = capacity(), capacity = oldCapacity;if (slowpath(isConstantEmptyCache())) {if (!capacity) capacity = INIT_CACHE_SIZE;//默认值为4reallocate(oldCapacity, capacity, /* freeOld */false);}else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {// 缓存没超过3/4或者7/8}
#if CACHE_ALLOW_FULL_UTILIZATIONelse if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {// 对于小容量缓存(<=8),允许完全利用存储空间}
#endifelse {capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;//两倍扩容if (capacity > MAX_CACHE_SIZE) {capacity = MAX_CACHE_SIZE;}reallocate(oldCapacity, capacity, true);//重新申请空间}bucket_t *b = buckets();//可以吧bucket_t理解为哈希表mask_t m = capacity - 1;mask_t begin = cache_hash(sel, m);mask_t i = begin;//开放寻址法解决哈希冲突do {if (fastpath(b[i].sel() == 0)) {incrementOccupied();b[i].set<Atomic, Encoded>(b, sel, imp, cls());return;}if (b[i].sel() == sel) {// The entry was added to the cache by some other thread// before we grabbed the cacheUpdateLock.return;}} while (fastpath((i = cache_next(i, m)) != begin));bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}static inline mask_t cache_hash(SEL sel, mask_t mask) 
{return (mask_t)(uintptr_t)sel & mask;
}//哈希算法static inline mask_t cache_next(mask_t i, mask_t mask) {return (i+1) & mask;
}//解决哈希冲突的方法

我们可以发现这个insert函数一共可以分为两个部分,一个是扩容判断,一个是插入缓存,在存储的时候使用哈希表,就是说明这些方法存放的地址并不一定连续。

我们可以看到_occupied代表的其实就是bucket进行内部存储的函数的个数,对于这个_occupied有几个点是值得注意的

  1. alloc申请空间时,此时的对象已经创建,如果再调用init方法,occupied也会+1
  2. 有属性赋值时,会隐式调用set方法,occupied也会增加,即有几个属性赋值,occupied就会在原有的基础上加几个
  3. 新的方法被调用时,occupied也会增加,即有几次调用,occupied就会在原有的基础上加几个

接着我们来理解的就是这个扩容操作

  • 首先如果缓存为空的话,就给buckets分配初始化的长度(x86_64为4,arm为2)并且创建一个buckets
  • 在arm64框架下,缓存的大小 <= buckets长度的7/8,并且buckets长度<=8没装满8,不扩容,在x86_64下,缓存的大小 <= buckets长度的3/4 ,不扩容。
  • 扩容逻辑:对当前容量的2倍扩容,并且如果扩容后容量大小 > MAX_CACHE_SIZE,则设置为MAX_CACHE_SIZE;计算出扩容大小后,以这个容量去创建新的buckets,和释放旧的buckets

这就是为什么当我们调用类之中-[GGObject sayHello]方法之后,反而_occupied变回了1,这就是因为旧值的buckets已经被完全释放,重新开辟了一个内存。

以下是开辟bucket内存reallocate方法的源码实现

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{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) {collect_free(oldBuckets, oldCapacity);}
}

_bucketsAndMaybeMask

ARM64 架构 下,_bucketsAndMaybeMask 被设计为 复合字段

  • 高位存储 mask(16 位):capacity - 1
  • 低位存储 buckets 地址(48 位):指向 bucket_t 数组的指针。(因为ARM64 架构下虚拟地址为48位)
mask_t cache_t::mask() const
{uintptr_t maskAndBuckets = _bucketsAndMaybeMask.load(memory_order_relaxed);return maskAndBuckets >> maskShift;//maskShift大小为48位
}
struct bucket_t *cache_t::buckets() const
{uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);return (bucket_t *)(addr & bucketsMask);
}

程序可以通过以上形式分别获取mask和bucket。

参考文章

iOS-底层原理 11:objc_class 中 cache 原理分析

iOS底层-cache_t原理分析

Objective-C 类的底层探索

http://www.xdnf.cn/news/378811.html

相关文章:

  • C32-编程案例用函数封装获取两个数的较大数
  • IPFS与去中心化存储:重塑数字世界的基石
  • nuscenes_devkit工具
  • Windows:Powershell的使用
  • 进阶二:基于HC-SR04和LCD1602的超声波测距
  • 海纳思(Hi3798MV300)机顶盒遇到海思摄像头
  • 贪心算法专题(Part1)
  • AI大模型学习十七、利用Dify搭建 AI 图片生成应用
  • STL-to-ASCII-Generator 实用教程
  • SpringBoot2集成xxl-job详解
  • 大模型微调指南之 LLaMA-Factory 篇:一键启动LLaMA系列模型高效微调
  • 差动讯号(3)弱耦合与强耦合
  • Linux数据库篇、第一章_01MySQL5.7的安装部署
  • Java基础 5.10
  • 致远A8V5-9.0安装包(包含信创版)【附百度网盘链接】
  • LeetCode 热题 100 24. 两两交换链表中的节点
  • 计算机网络八股文--day1
  • suricata之日志截断
  • Python实例题:Python协程详解公开课
  • JAVA练习题(1) 卖飞机票
  • vue开发用户注册功能
  • 【入门】数字走向I
  • 求数组中的两数之和--暴力/哈希表
  • 构建休闲企业服务实训室:融合凯禾瑞华产品打造产教融合新生态
  • 红黑树删除的实现与四种情况的证明
  • 北京导游资格证备考单选题题库及答案【2025年】
  • 大型旋转机械信号分解算法模块
  • 猿人学第十二题-js入门
  • c++——二叉树进阶
  • SAP Commerce(Hybris)开发实战(一)