前言: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的不断探索之旅,希望能有帮助到各位读者朋友。
复制代码
目录如下:
- iOS 底层原理探索 之 alloc
- iOS 底层原理探索 之 结构体内存对齐
- iOS 底层原理探索 之 对象的本质 & isa的底层实现
- iOS 底层原理探索 之 isa – 类的底层原理结构(上)
写在前面
在之前的 iOS 底层原理探索 之 isa – 类的底层原理结构(上) 文章中,我们分析了isa的走位,以及类的继承的关系, 并且 结合isa和类的继承综合做了分析;再接着我们探索来类的底层结构,并且重点分析了class_data_bits_t bits
,和其中的class_rw_t* data
以及其内部的properties
、methods
、protocols
和 ro
– ivars
并且验证了我们对于类方法存储在元类中的猜想是正确的。今天,我们继续探索类的底层原理结构之类的 cache
。
cache
缓存的意思,那么,要么缓存方法,要么缓存属性。我们根据探索bits
的节奏,也是通过内存平移来一步一步探索cache
。 通过上一篇的总结,我们知道,偏移到bits
是32字节,偏移到cache
是16字节。
准备
IMP
与 SEL
的关系
SEL
: 方法编号 (相当于书本中目录的名称)IMP
: 函数指针地址 (相当于书本中目录的页码)
首先,我们明白要找到书本中的什么内容 (SEL
目录里面的名称);然后,通过名称找对应的页码 (IMP
);最后,通过页码去定位具体的内容。
cache_t
是什么数据结构
首先我们看下 cache_t
的内存结构
从内存结构中并不能直接看到cache_t
中的那个属性是我们想要的内容,那么,根据我们对于cache
的了解, 要缓存方法或者属性,那么,一定会提供一些关于增删改查的方法,所以我们去看下cache_t
所提供的方法,知道我们看到了有一个insert
方法
不要犹豫,果断点进去看下实现
可以看到 insert
的过程中 对一个 bucket_t
的结构体做了操作。
接下来,我们就具体看下 bucket_t
的内容
果不其然,在bucket_t
这里看到 sel
和 imp
。
到这一步,我们简答总结下 cache_t
的数据结构:
LLDB 验证 方法的存储
接下来,我们要做一下验证,来查看下里面的内容:
那么,我们可以让 SMPerson
的实例 p
执行一下实例方法
接着,再获取一遍cache_t
中buckets()
,我们发现还是没有内容
那么,同样的方法,我们再去buckets_t
里面查找看看相关的方法,
果然,找到了 熟悉的 sel()
和 imp()
那么,继续LLDB
cache_t
流程分析
首先cache_t
是一个结构体, _bucketsAndMaybeMask
是一个 uintptr_t
也就是 unsigned long
, 其内部存储的是两部分数据正如其名字一样,是 buckets
和maybeMask
。
作为缓存,首先需要有写接着可以读。 所以 我们顺着思路找到cache_t
的
insert()
void insert(SEL sel, IMP imp, id receiver);
复制代码
通过方法也可以明白,插入的是sel和imp到receiver中。
INIT_CACHE_SIZE
为 4
_maybeMask.store
类中的cache在实例调用方法的时候,就开始进行存储了,首先新创建一个容器 bucket,其中的imp不是直接存储,而是将8字节的地址指针存储器中。否则,会占用很大的内存空间。在需要读取类信息的时候,就顺着这个指针地址去找寻实现。这样,不至于每次读取类信息会很慢。
所以, 此时 occupied = 1, maybeMask = 3;
buckets是一个数组,插进来的数据存储到哪里,此时,并不知道,在这里会进行cache_hash算法,这样可以得到一个哈希地址。
cache_hash
接下来就开始寻找,在sel()
不存在的时候,先对occupied++, 然后在 bucket_t
–b
合适的位置中,将 sel
imp
cls()
插入进去。
如果 sel()
已经存在,那么就不在缓存。
incrementOccupied()
之后继续存储的时候,会判断是否超出了75%的容量,没有,正常存储;超出,则 判断 capacity存在后, 进行 2倍的
扩容操作(原来的 4 * 2 = 8, m = 8 – 1 = 7)。扩容后,会将,之前存储的内容一并清掉,只会有一个新存储的方法的sel
imp
。<重新开辟一个新的buckets
, 将旧的bucket和capacity 内存地址 清空回收>
cache_fill_ratio
collect_free
为什么不直接追加,而是扩容后清空呢?
原来我们已经开辟的内存是无法改的,开辟新的内存空间后是一个新的地址,原来存储的值并不拿过来,是因为内存在数组平移的时候是十分耗费性能的,并且,苹果在底层的一个原则是越新越好
。 在扩容时,之前调用的方法,再一次被使用的几率时很低的,然而,扩容之时的调用的那个方法,是很重要的,所以,系统会见只将扩容调用的那次做存储。
当然,我们可以作为探索,在扩容之后,再次调用之前的方法,发现,在cache中是会继续添加进来的;不过此时,是无序的因为上面的cache_hash
算法。
最终 cache_t的流程
细节
_bucketsAndMaybeMask
bucket()
取值过程中,通过 _bucketsAndMaybeMask
的指针平移才可以获取其他的buckets();
bucket()
拿到 _bucketsAndMaybeMask
的地址,然后做 与
操作强转成 bucket_t *
bucketsMask
在不同架构下,值是不同的
CACHE_MASK_STORAGE