iOS底层-cache_t流程分析
2020/7/7 23:26:46
本文主要是介绍iOS底层-cache_t流程分析,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
本文概述
本文旨在通过源码分析cache_t相关,如缓存策略,动态扩容等,了解方法查找的流程。因其与objc_msgSend流程有密切联系,而发送消息又是iOS方法的本质,故了解cache_t是有必要的。
cache_t初探
1.cache_t 结构
上一篇iOS底层 -- 类的本质分析中对cache_t结构有初步的了解,其底层的结构:
struct objc_class : objc_object { // Class ISA; Class superclass; cache_t cache; class_data_bits_t bits; .. } 复制代码
cache_t
位于objc_class
中
struct cache_t { struct bucket_t *_buckets; mask_t _mask; mask_t _occupied; } 复制代码
自身的结构由三个属性组成:
buckets
是装方法的桶子,里面放着方式实现imp
,根据方法编号sel
生成的key
mask
是扩容因子,同时和key
共同计算得出寻找bucket_t
的哈希索引occupied
是当前占用的容量
2.lldb调试
在objc源码中新建CJPerson类,自定义四个方法,在外部对其进行调用
CJPerson * person = [[CJPerson alloc]init]; Class cls1 = object_getClass(person); [person firstAction]; Class cls2 = object_getClass(person); [person secondAction]; Class cls3 = object_getClass(person); [person thirdAction]; Class cls4 = object_getClass(person); [person fourthAction]; Class cls5 = object_getClass(person); 复制代码
在每个方法调用前输出下各个cls,以cls1为例,在[person firstAction]处打断点
x/4gx
打印cls1
内存地址的前四位,因为cache_t
前面的两个属性isa
和superclass
合占16字节,故cache_t
的地址等于isa
的地址偏移16个字节,即是图中标注的地址,强转输出这个地址得到cls1
的cache_t
内的属性值,依次类推,输出其他的cls
类对象 | _mask | _occupied |
---|---|---|
cls1 | 3 | 1 |
cls2 | 3 | 2 |
cls3 | 3 | 3 |
cls4 | 7 | 1 |
cls5 | 7 | 2 |
根据这个结果,会发现前面三个cls
还有一点规律可循,occupied
每次+1,可是到cls4
的时候,这个规律又不符合了,并且mask
的值也发生了变化。看来只看打印结果是没办法看出是谁在作怪,还是要去源码瞧一瞧。
cache_t源码
1.寻找切入点
虽说从源码入手这个想法是好的,可是那么多源码,究竟要从哪里看起。思来想去,还是先搜索下cache_t
,
cache_t
是个结构体,那系统应该会
初始化它,然后在操作它,那尝试搜索下cache_t *
,
只有4条相关结果,这个压力瞬间减少,一个个查看也没什么问题了。
1.第一条
cache_t *getCache(Class cls) { assert(cls); return &cls->cache; } 复制代码
这只是个get
方法,排除
2.第二条
void cache_t::bad_cache(id receiver, SEL sel, Class isa) { // Log in separate steps in case the logging itself causes a crash. _objc_inform_now_and_on_crash ("Method cache corrupted. This may be a message to an " "invalid object, or a memory error somewhere else."); cache_t *cache = &isa->cache; _objc_inform_now_and_on_crash ("%s %p, SEL %p, isa %p, cache %p, buckets %p, " "mask 0x%x, occupied 0x%x", receiver ? "receiver" : "unused", receiver, sel, isa, cache, cache->_buckets, cache->_mask, cache->_occupied); ... } 复制代码
这是在bad_cache
方法中,看名字和实现,不难判断出,这是坏缓存的时候发出的警告和崩溃消息
3.第三条
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver) { cacheUpdateLock.assertLocked(); if (!cls->isInitialized()) return; if (cache_getImp(cls, sel)) return; cache_t *cache = getCache(cls); ... } 复制代码
这是在cache_fill_nolock
方法中,方法名意为缓存填充,感觉可能是这个,但还不能确定
4.第四条
void cache_erase_nolock(Class cls) { cacheUpdateLock.assertLocked(); cache_t *cache = getCache(cls); ... } 复制代码
这是在cache_erase_nolock
方法中,方法名意为缓存擦除
综上所述,第三条的可能性是最高的,但还需要验证,在每条方法内打断点,[person firstAction]调用后,看下执行结果
断点来到cache_fill_nolock
内,可以确认方法调用后,是执行这个方法进行缓存的。
2.cache_fill_nolock解析
确认了这是方法调用后执行缓存的方法,那就有必要对其实现做深入的研究了。
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver) { cacheUpdateLock.assertLocked(); ① if (!cls->isInitialized()) return; ② if (cache_getImp(cls, sel)) return; ③ cache_t *cache = getCache(cls); ④ cache_key_t key = getKey(sel); ⑤ mask_t newOccupied = cache->occupied() + 1; ⑥ mask_t capacity = cache->capacity(); ⑦ if (cache->isConstantEmptyCache()) { ⑧ cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE); ⑨ } else if (newOccupied <= capacity / 4 * 3) { ⑩ } else { cache->expand(); ⑪ } bucket_t *bucket = cache->find(key, receiver); ⑫ if (bucket->key() == 0) cache->incrementOccupied(); ⑬ bucket->set(key, imp); ⑭ } 复制代码
① 缓存锁 ② 判断类是否被初始化,如果未初始化,直接返回 ③ 判断sel是否已经被cls缓存,如果已经缓存就没必要再走下面的缓存流程,直接返回 ④ 通过cls获取cache_t的内存地址 ⑤ 通过sel强转生成key ⑥ 获取cache中当前占有量occupied + 1的值,给下面进行容量判断 ⑦ 取出cache中的容量capacity ⑧ 判断当前占有量是否为0并且桶子的容量是否为空的 ⑨占有量为0并且桶子容量为空时,进行reallocate重新分配Buckets和Mask ⑩新的占有量小于等于总容量的3/4时,无另外操作 ⑪新的占有量大于总容量的3/4时,进行expand扩容 ⑫ 通过key当作哈希下标寻找对应bucket ⑬ 如果key等于0,说明没找到,需要缓存,则cache中的当前占有量 occupied + 1 ⑭ 把key和imp装进桶子bucket 复制代码
总结:
cache_fill_nolock
先获取类的cache
,判断cache
为空时,创建cache
,如果cache
已经存在,判断存储后的占有量大于容量的3/4时,就扩容。做完这些后,key
和imp
放入bucket
中,把bucket
放入cache
中。
明白了缓存的主流程,在对里面比较重要的方法,如reallocate
和expand
等做进一步解析
a.reallocate(mask_t oldCapacity, mask_t newCapacity)
INIT_CACHE_SIZE定义
enum { INIT_CACHE_SIZE_LOG2 = 2, INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2) }; 复制代码
实现
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity) { ... setBucketsAndMask(newBuckets, newCapacity - 1); if (freeOld) { cache_collect_free(oldBuckets, oldCapacity); cache_collect(false); } } 复制代码
可以看到,在第一次调用时,capacity
为空,则传入4,然后给setBucketsAndMask
入参,重新生成_buckets
,并且最后需要丢弃旧的桶子oldBuckets
,清空原来的缓存
void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask) { mega_barrier(); _buckets = newBuckets; mega_barrier(); _mask = newMask; _occupied = 0; } 复制代码
处理完后,_mask
= 4 - 1 = 3,_occupied
= 0。mask = newCapacity - 1,newCapacity = 2^n(初始化最小为4,n为扩容次数+2),故mask = 2^n - 1,lldb调试的值得以验证
b.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(oldCapacity, newCapacity); } 复制代码
先获取oldCapacity
旧容量,
mask_t cache_t::capacity() { return mask() ? mask()+1 : 0; } 复制代码
等于mask + 1,初始化后mask = 3 ,那这里oldCapacity
= 4,新容量newCapacity
等于旧容量oldCapacity
* 2 = 8,然后给reallocate
入参,重新分配桶子buckets
和mask
c.find(cache_key_t k, id receiver)
bucket_t * cache_t::find(cache_key_t k, id receiver) { assert(k != 0); bucket_t *b = buckets(); mask_t m = 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); Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache)); cache_t::bad_cache(receiver, (SEL)k, cls); } 复制代码
通过cache_hash
函数【begin = k & m】计算出key
值 k
对应的 index
值begin
,用来记录查询起始索引,然后把begin
赋值给 i
,用于切换索引。
用这个i
从散列表取值,如果取出来的bucket_t
的 key
= k
,则查询成功,返回该bucket_t
;如果key
= 0,说明在索引i
的位置上还没有缓存过方法,同样需要返回该bucket_t
,用于中止缓存查询。然后while循环其实就相当于绕圈,把散列表头尾连起来,从begin
值开始,递减索引值,当走过一圈之后,必然会重新回到begin
值,如果此时还没有找到key
对应的bucket_t
,或者是空的bucket_t
,则循环结束,说明查找失败,调用bad_cache
方法抛出异常。
cache_t部分问题汇总
1.类的cache_t会对任何方法进行缓存嘛?
类的cache_t
只会缓存对象方法,类方法缓存在元类中
2.为什方法编号sel需要强转成key?
方法编号sel是字符串类型,在底层传递的效率不如long类型的key
3.mask的作用?
mask
做为判断是否扩容的参数之一,可以称之为扩容因子;同时mask
和key
共同生成哈希下表用来查找bucket_t
。这里的mask
和isa_mask
做为面具的功能不一样,需要进行区分。
4.方法缓存是否有序?
方法缓存的bucket_t
是根据哈希下表得到的,找到空位子就直接坐下占有,不存在有序
5.为什么扩容是3/4?
HashMap
的负载因子等于0.75时,空间利用率比较高,而且避免了相当多的Hash冲突,使得底层的链表或者是红黑树的高度比较低,提升了空间效率。
6.bucket和mask,capacity,sel和imp的关系
bucket
内装着imp
和sel
生成的key
bucket
是通过key
和mask
进行寻找的bucket
的数量取决于capacity
7.为什么扩容后需要丢弃原先所有的缓存?
感觉上,扩容后丢弃缓存,下次调用在重新缓存的行为使得缓存变的没有意义。实际上,一般类中有较多方法,如果扩容后,还需要保存原先的方法,新bucket_t
需要从旧的bucket_t
中获取所有方法,在一个个放入,这本身是比较耗时的行为。方法缓存的目的就是使消息发送流程更快速,苹果为了更快,还使方法查找走汇编流程,总之这一切都是为了一个字--快。扩容后丢弃原先缓存就是在这个目的下被设计出来,而且这样设计,从某种意义上来说,也符合LRU
缓存策略,最近被缓存的方法使用率也较高,较早被缓存的使用率较低。
写在最后
以上就是我对cache_t
的部分理解,cache_t
是消息发送的一个切入点。下一章objc_msgSend
会对cache_t
有一个更宏观的理解。敬请期待。
这篇关于iOS底层-cache_t流程分析的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2022-10-05Swift语法学习--基于协议进行网络请求
- 2022-08-17Apple开发_Swift语言地标注释
- 2022-07-24Swift 初见
- 2022-05-22SwiftUI App 支持多语种 All In One
- 2022-05-10SwiftUI 组件参数简写 All In One
- 2022-04-14SwiftUI 学习笔记
- 2022-02-23Swift 文件夹和文件操作
- 2022-02-17Swift中使用KVO
- 2022-02-08Swift 汇编 String array
- 2022-01-30SwiftUI3.0页面反向传值