iOS 底层原理之 cache结构分析(上)

类的原理分析中提到过类的成员变量,其中就包含了cache_t,那么本篇就从cache_t开始分析

一.cache_t结构分析

首先附一张cache_t原理分析图

在这里插入图片描述

1.cache_t的源码分析

首先在源码中看一下cache的大致结构

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask; // 4
#if __LP64__
            uint16_t                   _flags;  // 2
#endif
            uint16_t                   _occupied; // 2
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 8
    };
    //部分代码

通过源码大致分析尝试一下,通过LLDB调式打印,代码如下

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        LGPerson *p  = [LGPerson alloc];
        Class pClass = [LGPerson class];
        [p saySomething];
        NSLog(@"%@",pClass);
        

    }
    return 0;
}

开始断点调试

//获取cache_t 内部属性
(lldb) p/x pClass
(Class) $0 = 0x0000000100004518 LGPerson
(lldb) x/4gx $0
0x100004518: 0x0000000100004540 0x0000000100353140
0x100004528: 0x000000010034b380 0x0000802800000000

(lldb) p (cache_t *)0x000000010034b380
(cache_t *) $2 = 0x000000010034b380
(lldb) p *$2
(cache_t) $3 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 0
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 0
        }
      }
      _flags = 0
      _occupied = 0
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0000000000000000
      }
    }
  }
}
(lldb) 

通过上面的调试发现,似乎没有什么有用的数据,那么只能继续查看源码,看有没有什么方法可以获取想要的数据,参照之前分析bit一样

    static constexpr uintptr_t bucketsMask = ~0ul;
    static_assert(!CONFIG_USE_PREOPT_CACHES, "preoptimized caches not supported");
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    static constexpr uintptr_t maskShift = 48;
    static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;//平移
    static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << maskShift) - 1;
    
    static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS, "Bucket field doesn't have enough bits for arbitrary pointers.");
#if CONFIG_USE_PREOPT_CACHES
    static constexpr uintptr_t preoptBucketsMarker = 1ul;
    static constexpr uintptr_t preoptBucketsMask = bucketsMask & ~preoptBucketsMarker;
#endif
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    // _bucketsAndMaybeMask is a buckets_t pointer in the low 48 bits
    // _maybeMask is unused, the mask is stored in the top 16 bits.

    // How much the mask is shifted by.
    static constexpr uintptr_t maskShift = 48;

    // Additional bits after the mask which must be zero. msgSend
    // takes advantage of these additional bits to construct the value
    // `mask << 4` from `_maskAndBuckets` in a single instruction.
    static constexpr uintptr_t maskZeroBits = 4;

    // The largest mask value we can store.
    static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
    
    // The mask applied to `_maskAndBuckets` to retrieve the buckets pointer.
    static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1;
    
    // Ensure we have enough bits for the buckets pointer.
    static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS,
            "Bucket field doesn't have enough bits for arbitrary pointers.");

#if CONFIG_USE_PREOPT_CACHES
    static constexpr uintptr_t preoptBucketsMarker = 1ul;
#if __has_feature(ptrauth_calls)
	//注释意思 应该是注明内存中64位分别代表什么,参考之前isa内存分析
    // 63..60: hash_mask_shift
    // 59..55: hash_shift
    // 54.. 1: buckets ptr + auth
    //      0: always 1
    static constexpr uintptr_t preoptBucketsMask = 0x007ffffffffffffe;
    static inline uintptr_t preoptBucketsHashParams(const preopt_cache_t *cache) {
        uintptr_t value = (uintptr_t)cache->shift << 55;
        // masks have 11 bits but can be 0, so we compute
        // the right shift for 0x7fff rather than 0xffff
        return value | ((objc::mask16ShiftBits(cache->mask) - 1) << 60);
    }

通过这段源码判断发现,cache_t的结构里面似乎某些数据也可以通过内存平移获取

    void reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld);
    void collect_free(bucket_t *oldBuckets, mask_t oldCapacity);

    static bucket_t *emptyBuckets();
    static bucket_t *allocateBuckets(mask_t newCapacity);
    static bucket_t *emptyBucketsForCapacity(mask_t capacity, bool allocate = true);
    static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);
    void bad_cache(id receiver, SEL sel) __attribute__((noreturn, cold));

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;

发现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

    // Compute the ptrauth signing modifier from &_imp, newSel, and cls.
    uintptr_t modifierForSEL(bucket_t *base, SEL newSel, Class cls) const {
        return (uintptr_t)base ^ (uintptr_t)newSel ^ (uintptr_t)cls;
    }

终于找到了,我们感兴趣的数据了,_imp,_sel,而且每一个sel对应一个imp,那么接下来就继续通过LLDB调试

2.LLDB调试分析cache_t

通过cache_t中的buckets()进行内存平移 ,首先得明白buckets()这里不是数组,通过前面的结构分析,是结构体指针,那么这里的平移,自然也是以(bucket_t *)作为单位进行平移,获取另1个bucket_t 的内存地址,而每一个结构体bucket_t 里面都有对应的_imp,_sel,结果如下

//查找cache_t 内部方法
(lldb) p/x pClass
(Class) $0 = 0x0000000100004500 LGPerson
(lldb) p (cache_t *)0x0000000100004510
(cache_t *) $1 = 0x0000000100004510
(lldb) p *$1
(cache_t) $2 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 4301545824
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 3
        }
      }
      _flags = 32808
      _occupied = 1
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0001802800000003
      }
    }
  }
}
(lldb) p $2.buckets()[0]//内存平移 这里buckets()不是数组
(bucket_t) $3 = {
  _sel = {
    std::__1::atomic<objc_selector *> = (null) {
      Value = (null)
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 0
    }
  }
}
(lldb) p $2.buckets()[1]
(bucket_t) $4 = {
  _sel = {
    std::__1::atomic<objc_selector *> = (null) {
      Value = (null)
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 0
    }
  }
}
(lldb) p $2.buckets()[2]
(bucket_t) $5 = {
  _sel = {
    std::__1::atomic<objc_selector *> = "" {
      Value = ""
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 30976
    }
  }
}

相信大家也发现了问题,就是打印p $2.buckets()[2]时,Value才是有值的,而前面都没有,而通过你添加调用更多的方法之后,这些Value有值的顺序的并不是有序的,这里面就涉及到哈希函数拉链法,这方面大家可以自行查阅,简单来说,通过这种方式主要目的是为了加快方法的插入读取效率,因为如果用数组去存取方法列表,就会非常影响插入和读取的效率,而哈希函数会使得每个buckets地址都有一个对应的哈希值,通过拉链法解决哈希算法的冲突问题,以便于增删改查

那么现在的问题是如何获取buckets里面的sel,imp,继续源码走起

 inline SEL sel() const { return _sel.load(memory_order_relaxed); }
 inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {

通过源码就可以看出调用对应函数即可

(lldb) p $6.imp(nil,pClass)
(IMP) $7 = 0x0000000100003c30 (KCObjcBuild`-[LGPerson sayNoBB])
(lldb) p $6.sel()
(SEL) $8 = "sayNoBB"

二.cache_t脱离源码结构分析

1.为什么要脱离源码分析

相信通过这几天的学习,大家都会发现,源码中各种数据,如结构体中,都会参杂很多静态变量,一些环境判断,又或者是函数的定义,实现等等,以至于我们在查看过程会很懵,有干扰,那么如果去掉这些我们不关心的代码,专心致志的分析我们想探索的内容,是不是更容易理清关系呢,那么接下我们就尝试一下,总结如下

  • 源码无法调试
  • 结构关系复杂,LLDB调试繁琐
  • 小规模取样 (不会因为内部其他的因素产生影响,理解更加简单清晰)

2.脱离源码分析

首先我们都知道_objc_class,主要的成员变量,那么我们也定义以下,说白了就是简化代码

// cache class
struct ycx_objc_class {
    Class isa;
    Class superclass;
    struct ycx_cache_t cache;             // formerly cache pointer and vtable
    struct ycx_class_data_bits_t bits;
};

然后是_class_data_bits_t

struct ycx_class_data_bits_t {
    uintptr_t bits;
};

接着是cache_t

typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits

//struct bucket_t *cache_t::buckets() const
//{
//    uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
//    return (bucket_t *)(addr & bucketsMask);
//}
//_bukets 替代_bucketsAndMaybeMask
struct ycx_cache_t {
    struct ycx_bucket_t *_bukets; // 8
    mask_t    _maybeMask; // 4
    uint16_t  _flags;  // 2
    uint16_t  _occupied; // 2
};

因为我们是要分析cache_tbucket_t,我们也定义一下

struct ycx_bucket_t {
    SEL _sel;
    IMP _imp;
};

接下来就开始代码尝试了,当然了以下代码是结合源码,经过多次尝试的结果

LGPerson *p  = [[LGPerson alloc]init];
Class pClass = p.class;  // objc_clas
[p say1];
[p say2];
[p say3];
[p say4];
[p say5];
[p say6];

[pClass sayHappy];
struct ycx_objc_class *ycx_class = (__bridge struct ycx_objc_class *)(pClass);
NSLog(@"%hu - %u",ycx_class->cache._occupied,ycx_class->cache._maybeMask);
        
for (mask_t i = 0; i<ycx_class->cache._maybeMask; i++) {
	struct ycx_bucket_t bucket = ycx_class->cache._bukets[i];
	NSLog(@"%@ - %pf",NSStringFromSelector(bucket._sel),bucket._imp);
}

打印结果如下

2021-06-24 15:55:12.950582+0800 003-cache_t脱离源码环境分析[17820:270757] 5 - 7
2021-06-24 15:55:12.950650+0800 003-cache_t脱离源码环境分析[17820:270757] say4 - 0x5910f
2021-06-24 15:55:12.950742+0800 003-cache_t脱离源码环境分析[17820:270757] say6 - 0x59b0f
2021-06-24 15:55:12.950788+0800 003-cache_t脱离源码环境分析[17820:270757] say3 - 0x5920f
2021-06-24 15:55:12.950854+0800 003-cache_t脱离源码环境分析[17820:270757] (null) - 0x0f
2021-06-24 15:55:12.950914+0800 003-cache_t脱离源码环境分析[17820:270757] say5 - 0x5940f
2021-06-24 15:55:12.950966+0800 003-cache_t脱离源码环境分析[17820:270757] say2 - 0x58f0f
2021-06-24 15:55:12.950997+0800 003-cache_t脱离源码环境分析[17820:270757] (null) - 0x0f

3.通过多次测试发现几个问题

  • 为什么会存在(null) - 0x0f
  • 类方法去哪里了
  • 5-7是什么意思
  • 为什么会有方法调用了没有打印出来

三.分析cache插入数据原理

1.cache插入

首先查找insert主要流程代码

void cache_t::insert(SEL sel, IMP imp, id receiver)
{
    runtimeLock.assertLocked();

    // Never cache before +initialize is done
    if (slowpath(!cls()->isInitialized())) {
        return;
    }

    if (isConstantOptimizedCache()) {
        _objc_fatal("cache_t::insert() called with a preoptimized cache for %s",
                    cls()->nameForLogging());
    }

#if DEBUG_TASK_THREADS
    return _collecting_in_critical();
#else
#if CONFIG_USE_CACHE_LOCK
    mutex_locker_t lock(cacheUpdateLock);
#endif

    ASSERT(sel != 0 && cls()->isInitialized());

    // Use the cache as-is if until we exceed our expected fill ratio.
    mask_t newOccupied = occupied() + 1; // 1+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);
    }
    // CACHE_END_MARKER 1 这里是和+1之后比较计算 做了预留
    else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
        // Cache is less than 3/4 or 7/8 full. Use it as-is.
        //超过75% 就扩容
    }
#if CACHE_ALLOW_FULL_UTILIZATION
    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 { //超过75%情况下  双倍扩容
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        //这里传的是true 说明扩容的情况下,就会清理旧的buckets内存
        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;

    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot.
    //防止哈希冲突,循环哈希,知道与初始不同
    do {
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();//_occupied++;  说白了每次插入一个新的bucket_t  _occupied++
            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
}

重新分配cache函数cache_t::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);
	//设置BucketsAndMask  插入空的容器(桶子)
    setBucketsAndMask(newBuckets, newCapacity - 1);
    //是否清空原来内存
    if (freeOld) {
        collect_free(oldBuckets, oldCapacity);
    }
}

2.源码分析cache插入算法并验证

通过上面的源码分析得出以下结论

  • 1.每个bucket_t(桶)对应一个sel,imp内存地址
  • 2.存储bucket_t(桶)的基础数值是capacity = INIT_CACHE_SIZE = 4
  • 3.等于3/4就双倍扩容(capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE)
  • 4.每次扩容是重新开辟新的空间存储(setBucketsAndMask(newBuckets, newCapacity - 1))
  • 5.每次扩容会清空原有的缓存collect_free(oldBuckets, oldCapacity),重新开始存储;
  • 6.每个bucket_t(桶)地址都是由哈希函数cache_hash(sel, m)计算得出(所以打印会发现并不是有序的)

接下来我们一起验证一下:

1.先不调用任何方法,可以看到默认会调用class

在这里插入图片描述
2.补充方法say1,可以发现_occupied是2,没有超过4的3/4也就是3,所以打印的方法say1,class

在这里插入图片描述
3.继续补充方法say2,等于3/4后,扩容翻倍在减1就是7,然后清空旧方法,就只剩下say2

在这里插入图片描述
4.再让他扩容一次,确定我们结论是否正确

在这里插入图片描述
今天就分析到这里了,更多精彩请听下回分解cache结构分析(下)!!!

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值