写在前面: 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
补充
架构和环境变量相关
bucket_t 获取IMP
inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
uintptr_t imp = _imp.load(memory_order_relaxed);
if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
SEL sel = _sel.load(memory_order_relaxed);
return (IMP)
ptrauth_auth_and_resign((const void *)imp,
ptrauth_key_process_dependent_code,
modifierForSEL(base, sel, cls),
ptrauth_key_function_pointer, 0);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
return (IMP)(imp ^ (uintptr_t)cls);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
return (IMP)imp;
#else
#error Unknown method cache IMP encoding.
#endif
}
这里传入了cls ,为什么 要 (IMP)(imp ^ (uintptr_t)cls); 有一个 ^ (异或) 的操作呢?意义是什么呢?
的IMP获取和存储是一个编码和解码的过程,在存储的时候,存储的是一个数值(无符号的长整形 存储到bucket_t 中)见表1,而不是一个纯粹 的指针地址,这也就意味着需要进行局部的操作,也就是编码在 _imp.load() 拿到存储到 imp 然后和 cls 进行按位异或。
- why? 为什么和 cls 按位异或 ?
- 因为在imp存储到时候,进行了编码操作,就是按位异或了 cls( 详见 表2 表3)。
- imp 和 sel 都是属于 cls 所以加密操作中盐值就是cls。
加密操作
- 一个数A异或另一个数B得到结果C; ( c = a ^ b )
- 结果C 异或了 数B 就会得到 数A; ( a = c ^ b )
- 这是一个算法规律。
验证
先插入我们 say666 到if代码到断点调试:
mian函数中调用 say666 方法:
- 验证下加密算法
表1
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__
explicit_atomic<uintptr_t> _imp;
explicit_atomic<SEL> _sel;
#else
explicit_atomic<SEL> _sel;
explicit_atomic<uintptr_t> _imp;
#endif
...
表2
template<Atomicity atomicity, IMPEncoding impEncoding>
void bucket_t::set(bucket_t *base, SEL newSel, IMP newImp, Class cls)
{
ASSERT(_sel.load(memory_order_relaxed) == 0 ||
_sel.load(memory_order_relaxed) == newSel);
// objc_msgSend uses sel and imp with no locks.
// It is safe for objc_msgSend to see new imp but NULL sel
// (It will get a cache miss but not dispatch to the wrong place.)
// It is unsafe for objc_msgSend to see old imp and new sel.
// Therefore we write new imp, wait a lot, then write new sel.
uintptr_t newIMP = (impEncoding == Encoded
? encodeImp(base, newImp, newSel, cls)
: (uintptr_t)newImp);
if (atomicity == Atomic) {
_imp.store(newIMP, memory_order_relaxed);
if (_sel.load(memory_order_relaxed) != newSel) {
#ifdef __arm__
mega_barrier();
_sel.store(newSel, memory_order_relaxed);
#elif __x86_64__ || __i386__
_sel.store(newSel, memory_order_release);
#else
#error Don't know how to do bucket_t::set on this architecture.
#endif
}
} else {
_imp.store(newIMP, memory_order_relaxed);
_sel.store(newSel, memory_order_relaxed);
}
}
表3
// Sign newImp, with &_imp, newSel, and cls as modifiers.
uintptr_t encodeImp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, IMP newImp, UNUSED_WITHOUT_PTRAUTH SEL newSel, Class cls) const {
if (!newImp) return 0;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
return (uintptr_t)
ptrauth_auth_and_resign(newImp,
ptrauth_key_function_pointer, 0,
ptrauth_key_process_dependent_code,
modifierForSEL(base, newSel, cls));
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
return (uintptr_t)newImp ^ (uintptr_t)cls;
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
return (uintptr_t)newImp;
#else
#error Unknown method cache IMP encoding.
#endif
}