iOS 底层第6
天的学习。今天又学了一个新的技能,原来还能把底层源码拉出来在局部进行分析,真是受益匪浅啊。
cache 探索
- 我们已经知道在类的底层结构中有
isa
,superclass
,bit
,cache
。
name | desc |
---|---|
isa |
isa 指针,objc isa -> Class ,Class isa -> metal Class
|
superclass |
父类 |
bit |
bit 里有 ro ,rw ,里有属性,方法,协议 等等 |
cache |
缓存 |
而这里
cache
里到底存了有哪些东西呢?
- 我们先
lldb
获取到cache
的内存地址,经过一番操作如下所示
(lldb) p/x pClass
(Class) $0 = 0x00000001000085d0 Person
(lldb) p/x 0x00000001000085d0 + 0x10 // 平移16字节
(long) $1 = 0x00000001000085e0
(lldb) p (cache_t *)0x00000001000085e0
(cache_t *) $2 = 0x00000001000085e0
(lldb) p *$2
(cache_t) $3 = {
_bucketsAndMaybeMask = {
std::__1::atomic<unsigned long> = {
Value = 4298515360
}
}
= {
= {
_maybeMask = {
std::__1::atomic<unsigned int> = {
Value = 0
}
}
_flags = 32800
_occupied = 0
}
_originalPreoptCache = {
std::__1::atomic<preopt_cache_t *> = {
Value = 0x0000802000000000
}
}
}
}
- 接下里就是要去分析
cache_t
源码,目的是为了找到相应的方法。
struct cache_t {
..... 省略部分代码
public:
// The following four fields are public for objcdt's use only.
// objcdt reaches into fields while the process is suspended
// hence doesn't care for locks and pesky little details like this
// and can safely use these.
unsigned capacity() const;
struct bucket_t *buckets() const;
Class cls() const;
#if CONFIG_USE_PREOPT_CACHES
const preopt_cache_t *preopt_cache() const;
#endif
mask_t occupied() const;
void initializeToEmpty();
#if CONFIG_USE_PREOPT_CACHES
bool isConstantOptimizedCache(bool strict = false, uintptr_t empty_addr = (uintptr_t)&_objc_empty_cache) const;
bool shouldFlush(SEL sel, IMP imp) const;
bool isConstantOptimizedCacheWithInlinedSels() const;
Class preoptFallbackClass() const;
void maybeConvertToPreoptimized();
void initializeToEmptyOrPreoptimizedInDisguise();
#else
inline bool isConstantOptimizedCache(bool strict = false, uintptr_t empty_addr = 0) const { return false; }
inline bool shouldFlush(SEL sel, IMP imp) const {
return cache_getImp(cls(), sel) == imp;
}
inline bool isConstantOptimizedCacheWithInlinedSels() const { return false; }
inline void initializeToEmptyOrPreoptimizedInDisguise() { initializeToEmpty(); }
#endif
void insert(SEL sel, IMP imp, id receiver);
}
- 我们看到 有 一个
struct bucket_t *buckets() const;
buckets
的结构体,
点击进入bucket_t
看一下。
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
}
发现了 _imp
和 _sel
,由此我们可以知道 buckets
里面存储了 _imp
好 `_sel,
- 接下来我们回到
lldb
去验证一下buckets
(lldb) p $3.buckets()
(bucket_t *) $5 = 0x00000001003623a0
(lldb) p *$5
(bucket_t) $6 = {
_sel = {
std::__1::atomic<objc_selector *> = (null) {
Value = nil
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 0
}
}
}
- 为什么这里的
Value
是nil
呢? 原来是buckets
是一个哈希结构.
(lldb) p $3.buckets()[1]
(bucket_t) $6 = {
_sel = {
std::__1::atomic<objc_selector *> = "" {
Value = ""
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 48880
}
}
}
- 查找
bucket_t
源码 ,发现2个方法
inline SEL sel() const { return _sel.load(memory_order_relaxed); }
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
}
继续 lldb
输出 sel
& imp
(lldb) p $6.sel()
(SEL) $8 = "doSomething"
(lldb) p $6.imp(nil,pClass)
(IMP) $12 = 0x0000000100003b00 (KCObjcBuild`-[Person doSomething])
- 验证成功。但是每次都要去
lldb
打印 感觉真的好麻烦,有没有更方便的方法呢?接下来我们用底层局部代码块的形式继续分析,用NSLog
日志方式进行打印。
局部代码分析 cache
- 把获取的源码更改成自己需要的
struct
struct x_bucket_t {
SEL _sel;
IMP _imp;
};
struct x_cache_t {
uintptr_t _bucketsAndMaybeMask;
mask_t _maybeMask;
uint16_t _flags;
uint16_t _occupied;
};
struct x_class_data_bits_t {
uintptr_t bits;
};
struct x_objc_class {
// Class ISA;
Class isa;
Class superclass;
struct x_cache_t cache;
struct x_class_data_bits_t bits;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [Person alloc];
Class p_class = p.class;
// 进行 x_objc_class 的强转
struct x_objc_class *x_class = (__bridge struct x_objc_class *)p_class;
NSLog(@"x_class is %@",x_class);
}
- 打印结果
x_class is Person
- 说明这样是可以底层源码更进行
NSLog
输出的,我们继续往下走。 - 我们的目的是要打印
x_cache_t
里的buckets
,可现在x_cache_t
里没有,我想能不能在x_cache_t
加一个x_bucket_t
试试呢,我们把_bucketsAndMaybeMask
替换成struct x_bucket_t *_buckets;
struct x_cache_t {
struct x_bucket_t *_buckets;
// uintptr_t _bucketsAndMaybeMask;
mask_t _maybeMask;
uint16_t _flags;
uint16_t _occupied;
};
Class p_class = p.class;
[p readBook1]; //
// 进行 x_objc_class 的强转
struct x_objc_class *x_class = (__bridge struct x_objc_class *)p_class;
for (mask_t i = 0 ; i<x_class->cache._maybeMask; i++) {
struct x_bucket_t bucket = x_class->cache._buckets[I];
NSLog(@"sel is %@, imp is %pf",NSStringFromSelector(bucket._sel),bucket._imp);
}
NSLog(@"_occupied is %hu,_maybeMask is %u",x_class->cache._occupied,x_class->cache._maybeMask);
- 打印输出结果,我们发现
_bucketsAndMaybeMask
替换成struct x_bucket_t *_buckets
可以输出结果打印sel
和imp
- 已经成功找到
sel
的readBook1
了
sel is (null), imp is 0x0f
sel is (null), imp is 0x0f
sel is readBook1, imp is 0xbcb0f
_occupied is 1,_maybeMask is 3
- 我们继续测试多打印几个方法调用,看看
cache
里_buckets
会有什么变化
Class p_class = p.class;
[p readBook1]; //
[p readBook2]; //
[p readBook3]; //
// 进行 x_objc_class 的强转
struct x_objc_class *x_class = (__bridge struct x_objc_class *)p_class;
for (mask_t i = 0 ; i<x_class->cache._maybeMask; i++) {
struct x_bucket_t bucket = x_class->cache._buckets[I];
NSLog(@"sel is %@, imp is %pf",NSStringFromSelector(bucket._sel),bucket._imp);
}
>>> readBook1
>>> readBook2
>>> readBook3
sel is (null), imp is 0x0f // 1
sel is (null), imp is 0x0f // 2
sel is (null), imp is 0x0f // 3
sel is (null), imp is 0x0f // 4
sel is (null), imp is 0x0f // 5
sel is (null), imp is 0x0f // 6
sel is readBook3, imp is 0xbc50f // 7
_occupied is 1,_maybeMask is 7
疑问1:为什么我调用了
3
个 方法,buckets
里的maybeMask
都变成7
个了呢?
疑问2:readBook2
,readBook3
为什么没有呢?
- 我们只调用
readBook1
和readBook2
看看输出如何
sel is readBook2, imp is 0xbc78f
sel is (null), imp is 0x0f
sel is readBook1, imp is 0xbc08f
_occupied is 2,_maybeMask is 3
- 我们可以看到如果是打印
2
个方法occupied
是2
, 但maybeMask
是3
没有增加, 打印1
个方法,occupied
是1
,maybeMask
还是3
,也没有增加。 - 我们带着上面的机个疑问,去分析下
cache
底层源码到底是如何实现的。 - 分析前先要思考下我们在调用 方法的时候是在从
cache
里读把,那到底是怎么写进去的呢?
struct cache_t {
...
void insert(SEL sel, IMP imp, id receiver);
...
}
- 我源码里找到了
insert
插入 = 写把,我们进入insert
方法里继续分析。
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
// ... 省略部分代码
// Use the cache as-is if until we exceed our expected fill ratio.
mask_t newOccupied = occupied() + 1; // occupied 默认 0 + 1
unsigned oldCapacity = capacity(), capacity = oldCapacity;
// 先从这里开始分析
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE; // 4
reallocate(oldCapacity, capacity, /* freeOld */false);
}
else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) { // <= capacity * 3/4
// Cache is less than 3/4 or 7/8 full. Use it as-is.
}
#if CACHE_ALLOW_FULL_UTILIZATION // 1
else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
// Allow 100% cache utilization for small buckets. Use it as-is.
}
#endif
else { // newOccupied +1 > capacity * 3/4 会进来
// 4*2 = 8
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);
}
bucket_t *b = buckets();
mask_t m = capacity - 1; // 4-1=3
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
//. 通过 do while 寻找合适的下标操作
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));
}
- 先
isConstantEmptyCache
判断是否有缓存,没缓存直接进入reallocate
方法开辟内存
ALWAYS_INLINE
/*
oldCapacity = 0
newCapacity = 4
freeOld = false
**/
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
bucket_t *oldBuckets = buckets();
bucket_t *newBuckets = allocateBuckets(newCapacity);
// ... 省略部分代码
// newCapacity = 4
setBucketsAndMask(newBuckets, newCapacity - 1);
if (freeOld) {
collect_free(oldBuckets, oldCapacity); // 回收清空
}
}
当
(3 = newOccupied +1) = {4 > 3} = (4 = capacity * 3/4 )
时会进入else
这个方法里。这里的3/4
只针对__arm__
||__x86_64__
||__i386__
当
capacity = capacity ? capacity * 2
->capacity = 8
->reallocate(oldCapacity, capacity, true)
->collect_free(oldBuckets, oldCapacity);
当
capacity = 8
->mask_t m = capacity - 1 = 7
就能解释 疑问1:为什么我调用了3
个 方法,buckets
里的maybeMask
都变成7
个了调用3 个 methods
=>occupied = 2
->else 代码
->freeOld = true
->重新梳理缓存
, 这样就能解释疑问2:readBook2
,readBook3
没有的原因了。
-
cache 分析图如下
总结
- 当
cache
里的occupied + 2
>capacity * 3/4
, 就会进行一次扩容,并把原有的旧方法都清空。而capacity
无值的时默认4
, 扩容后的capacity
=8
,扩容的算法就是当前capacity * 2
。 而maybeMask
=capacity - 1
知识点补充
- 脱离源码进行小规模底层分析到底有什么好处呢?
1:解决了有时源码无法调试的问题。
2:解决了lldb
操作麻烦的问题。
3:小规模取样 ,让你研究的东西 更加的简单和清晰。
ps: 真机架构 - arm64
模拟器 - i386
Mac - x86_64
_LP64 -> Unix 和 Unix类系统(Linux,Mac OS X)