iOS底层 - cache原理分析

iOS开发底层探究之路

在对Objective-C底层的探究过程中,已经探究过objc_class 结构中的isabits,那么本篇文章我们来见见这个cache是个啥玩意:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() const {
        return bits.data();
    }
}

以及cache_t结构体类型的cache源码:

struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    explicit_atomic _buckets;
    explicit_atomic _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic _maskAndBuckets;
    mask_t _mask_unused;
    
    // 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.");
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    // _maskAndBuckets stores the mask shift in the low 4 bits, and
    // the buckets pointer in the remainder of the value. The mask
    // shift is the value where (0xffff >> shift) produces the correct
    // mask. This is equal to 16 - log2(cache_size).
    explicit_atomic _maskAndBuckets;
    mask_t _mask_unused;

    static constexpr uintptr_t maskBits = 4;
    static constexpr uintptr_t maskMask = (1 << maskBits) - 1;
    static constexpr uintptr_t bucketsMask = ~maskMask;
#else
#error Unknown cache mask storage type.
#endif
    
#if __LP64__
    uint16_t _flags;
#endif
    uint16_t _occupied;
// 省略下方方法
}

其中,在不同运行环境解释为:

  • CACHE_MASK_STORAGE_OUTLINED 模拟器或者macOS
  • CACHE_MASK_STORAGE_HIGH_16 64位真机
  • CACHE_MASK_STORAGE_LOW_4 非64位真机
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 _imp;
    explicit_atomic _sel;
#else
    explicit_atomic _sel;
    explicit_atomic _imp;
#endif
void set(SEL newSel, IMP newImp, Class cls);
}

上面源码分析可以看出:

  • 类底层结构体中的 cachecache_t 结构体类型的,在 cache_t 结构体中,在不同的运行环境下,内部的结构也不一样,在模拟器macOS 下内部属性有 _buckets_mask ,在真机下将_buckets_mask存放在一起,为了节省内存,以_maskAndBuckets属性存放。
  • _buckets中存放的是bucket_t类型的数据,从bucket_t结构中可以看出只有属性_imp_sel,所以可以看出cache就是用来存放方法的。

cache例子分析

首先在可编译源码工程中创建一个LGPerson类,并添加属性及方法:

@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *lgName;
@property (nonatomic, strong) NSString *nickName;
- (void)sayHello;
- (void)sayCode;
- (void)sayMaster;
- (void)sayNB;
@end
#import "LGPerson.h"
@implementation LGPerson
- (void)sayHello{
    NSLog(@"LGPerson say : %s",__func__);
}
- (void)sayCode{
    NSLog(@"LGPerson say : %s",__func__);
}
- (void)sayMaster{
    NSLog(@"LGPerson say : %s",__func__);
}
- (void)sayNB{
    NSLog(@"LGPerson say : %s",__func__);
}
@end

main.m 文件main入口函数中创建实例并方法调用:

iOS底层 - cache原理分析_第1张图片
cache_t例子分析

如图,断点停在22行,在对象p调用实例方法之前,我们利用lldb打印当前pClass相关类信息,因为实例方法存放在类信息里面:

iOS底层 - cache原理分析_第2张图片
查看类cache信息

通过对当前pClass类指针偏移16位的地址进行打印,我们得到当前类的cache信息,其中_occupied = 0_mask = 0 ,好,我们继续下一个断点,来到了23行,此时只进行了sayHello方法的调用,我们再次打印当前类的cache信息:

iOS底层 - cache原理分析_第3张图片
第一次调用方法结果打印

可以看到:

  • 此时cache信息中_occupied = 1 ,那我们可以猜测这个属性是否就是表示当前方法调用的数量
  • _mask此时值为3
  • 通过cache_t结构体中的函数buckets(),我们可以获得buckets()指针地址,打印当前地址信息,得到一个bucket_t 结构体数据$5,再通过bucket_t结构体中的sel()imp(Class) 两个函数方法我们可以打印出刚刚调用的sayHello方法的方法名方法实现函数的地址信息。

好,接下来我们再调用第二个方法sayCode,再继续查看当前类的cahce信息:


iOS底层 - cache原理分析_第4张图片
第二个方法调用类信息打印
  • 此时cache信息中的_occupied = 2 ,那是否就证实我们上面所说的调用方法个数呢?下面继续探究!嘻嘻
  • _mask值不变依旧为3
  • 因为buckets数组,所以可以按地址偏移的方式进行遍历打印,也可以调用数组buckets()下标[0][1]...的方式获取。如上打印情况,偏移一位打印出$15 bucket_t 结构体信息,继而打印selimp信息,发现正是第二个方法sayCode

继续接着第三个方法sayMaster方法调用,那么此时再打印类信息又会是怎样的:

iOS底层 - cache原理分析_第5张图片
第三个方法调用结果打印

上图可以看出,在调用第三个方法之后,cache信息有了很大变化:

  • 此时_occupied = 1_mask = 7 ,首先这个_occupied = 1 就推翻了我们之前的猜想 _occupied 为 调用方法的个数;_mask 由先前的3变成了现在的7
  • 此时buckets第一个位置的bucket_tselnullimp = 0,那么我们利用上面所说的地址偏移或者下标打印是否还能打印出第三个方法的selimp信息?
    iOS底层 - cache原理分析_第6张图片
    第三个方法调用结果打印

    地址偏移1,刚好就找到了sayMasterselimp,而且发现其余位置打印出来的sel都为nullimp都为0

最后调用第四个方法sayNB,来看看结果又是怎样的:

iOS底层 - cache原理分析_第7张图片
第四方法调用结果

继续利用地址偏移,最后找到sayNB方法selimp 信息:
iOS底层 - cache原理分析_第8张图片
sayNB方法查找打印

从上面两个打印结果看出:

  • _occupied = 2 ,当我们调用第四个方法后,又加了1,然而经过第三个方法我们又证明了_occupied 与我们调用的方法数不想等,其实从字面意思上看得出_occupied是占用的意思,这到底代表啥呢?
  • _mask = 7 ,此时与第三个方法调用之后没啥改变,_mask又是啥呢?
  • 通过地址偏移,遍历打印,我们发现在buckets数组中有些的确存储了实例方法的selimp信息,但是有些是没存储任何方法信息(selnull,imp0),以及是否buckets中存储的方法信息一定是按方法调用先后顺序来存储的?

接下来我们通过分析cache缓存方法的源码来一探究竟。

cache 源码分析

拓展:查看源码原则,一般属性只有存储信息的作用,能改变信息的一般我们认准函数方法,函数方法才大有可能是改变信息的地方。

首先我们来看文章开始cache_t源码中隐藏的函数方法部分:

public:
    static bucket_t *emptyBuckets();
    
    struct bucket_t *buckets();
    mask_t mask();   
    mask_t occupied();
    void incrementOccupied();
    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
    void initializeToEmpty();

    unsigned capacity();
    bool isConstantEmptyCache();
    bool canBeFreed();
    void reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld);
    void insert(Class cls, SEL sel, IMP imp, id receiver);

    static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn, cold));

从上面所有函数方法中探究,前四个函数,查看源码,发现只是简单的获取cache的相对应的属性。我们发现第五个函数incrementOccupied()

void cache_t::incrementOccupied() 
{
    _occupied++;
}

可以看出此函数正式改变(增加)_occupied 值的,那我们就从此函数入手,全局搜索incrementOccupied(),发现只有void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver) 方法中找到此函数的调用位置:

iOS底层 - cache原理分析_第9张图片
incrementOccupied调用

从参数上来看,insert 方法可以理解为向cache中插入方法的selimp信息,全局搜索发现只有在cache_fill方法中进行了cache->insert调用:

void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
    runtimeLock.assertLocked();

#if !DEBUG_TASK_THREADS
    // Never cache before +initialize is done
    if (cls->isInitialized()) {
        cache_t *cache = getCache(cls);
#if CONFIG_USE_CACHE_LOCK
        mutex_locker_t lock(cacheUpdateLock);
#endif
        cache->insert(cls, sel, imp, receiver); //向cache插入方法sel、imp信息
    }
#else
    _collecting_in_critical();
#endif
}

全局搜索cache_fill,发现在cache_fill方法之前,还进行了Cache中的selimp 查找:

iOS底层 - cache原理分析_第10张图片
cache_fill

insert方法分析

下面着重分析 cache 缓存方法的核心函数insert源码:

iOS底层 - cache原理分析_第11张图片
cache::insert方法初探

我们可以将上面三个断点作为insert方法的三个阶段:

  • 阶段一:mask_t newOccupied = occupied() + 1;不管当前的cache是否已经初始化过,newOccupied都为当前的_occupied值加上1,表示当前cache内存所占用个数
  • 阶段二:初始化扩容阶段
    • 先判断当前cache是否是空的(未初始化),小概率事件判断,此时capacity()函数返回0oldCapacity = 0capacity = 0。如果cache为空,进入ifcapacity赋值为宏定义 4reallocate 去内存请求cache空间。
    • 如果当前cache已经初始化过了,newOccupied = _occupied + 1capacity()函数返回当前的cache容量大小,判断当前的newOccupied + CACHE_END_MARKER(1) <= capacity / 4 * 3是否满足 ,如果<=,继续下面的存储,如果>,则进入下一步的对cache 扩容
    • 对当前cache的容量大小扩大两倍,此时有最大值MAX_CACHE_SIZE(2^15) ,然后重新初始化当前的cache ,并对原有的cahce存储信息进行清理。接着进行下面存储工作。
  • 阶段三:真正的方法信息selimp存储步骤,bucket_t存放selimpbuckets存放bucket_t,所以cache缓存方法实质就是将bucket_t通过hash算法插入到buckets中的过程。

reallocate 初始化方法分析

ALWAYS_INLINE
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);

    setBucketsAndMask(newBuckets, newCapacity - 1);
    
    if (freeOld) {
        cache_collect_free(oldBuckets, oldCapacity);
    }
}

首先去内存中申请buckets内存空间,allocateBuckets函数:

bucket_t *allocateBuckets(mask_t newCapacity)
{
    // Allocate one extra bucket to mark the end of the list.
    // This can't overflow mask_t because newCapacity is a power of 2.
    bucket_t *newBuckets = (bucket_t *)
        calloc(cache_t::bytesForCapacity(newCapacity), 1);

    bucket_t *end = cache_t::endMarker(newBuckets, newCapacity);

#if __arm__   //真机
    // End marker's sel is 1 and imp points BEFORE the first bucket.
    // This saves an instruction in objc_msgSend.
    end->set((SEL)(uintptr_t)1, (IMP)(newBuckets - 1), nil);
#else          //模拟器或macOS(当前环境)
    // End marker's sel is 1 and imp points to the first bucket.
    end->set((SEL)(uintptr_t)1, (IMP)newBuckets, nil);
#endif
    
    if (PrintCaches) recordNewCache(newCapacity);

    return newBuckets;
}

通过calloc 函数去内存中申请newCapacity 大小的容量的newBuckets,此方法返回的newBuckets只是一个临时变量。setBucketsAndMask就是 将newBuckets保存在cache中:

  • CACHE_MASK_STORAGE_OUTLINED 模拟器macOS
    _buckets.store(newBuckets, memory_order::memory_order_release);
     
     // ensure other threads see new buckets before new mask
     _mask.store(newMask, memory_order::memory_order_release);
     _occupied = 0;
    
  • CACHE_MASK_STORAGE_HIGH_16 64位真机
    uintptr_t buckets = (uintptr_t)newBuckets;
     uintptr_t mask = (uintptr_t)newMask;
     
     ASSERT(buckets <= bucketsMask);
     ASSERT(mask <= maxMask);
     
     _maskAndBuckets.store(((uintptr_t)newMask << maskShift) | (uintptr_t)newBuckets, std::memory_order_relaxed);
     _occupied = 0;
    
  • CACHE_MASK_STORAGE_LOW_4 32位真机
    uintptr_t buckets = (uintptr_t)newBuckets;
     unsigned mask = (unsigned)newMask;
     
     ASSERT(buckets == (buckets & bucketsMask));
     ASSERT(mask <= 0xffff);
     
     // The shift amount is equal to the number of leading zeroes in
     // the last 16 bits of mask. Count all the leading zeroes, then
     // subtract to ignore the top half.
     uintptr_t maskShift = __builtin_clz(mask) - (sizeof(mask) * CHAR_BIT - 16);
     ASSERT(mask == (0xffff >> maskShift));
     
     _maskAndBuckets.store(buckets | maskShift, memory_order::memory_order_relaxed);
     _occupied = 0;
     
     ASSERT(this->buckets() == newBuckets);
     ASSERT(this->mask() == newMask);
    

对比可以看出,真机环境下,bucketmask的位置存储,并将occupied占用设置为0模拟器或者macOS下,正常存储bucketmask,并将occupied占用设置为0

最后判断是否需要清理oldBuckets,如果需要则调用cache_collect_free将旧的buckets清理掉。

iOS底层 - cache原理分析_第12张图片
清理旧buckets

hash 算法存储sel、imp

static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    return (mask_t)(uintptr_t)sel & mask;
}

cache_hash通过(SEL sel, mask_t mask) 中的sel & mask,把得到的作为下标在buckets获取当前的bucket_t,如果当前的bucket中的sel == 0 ,说明当前位置暂无插入任何selimp,即插入当前的selimp,并且_occupied占用加1;如果当前bucket中的sel与要插入的sel相等,说明已经存在了当前的sel,就直接返回,避免重复插入;如果当前bucket已经存在sel,即已经保存了别的方法的selimp信息,那就通过cache_next哈希冲突方法重新查找别的位置进行插入。

#if __arm__  ||  __x86_64__  ||  __i386__
// objc_msgSend has few registers available.
// Cache scan increments and wraps at special end-marking bucket.
#define CACHE_END_MARKER 1
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}

#elif __arm64__
// objc_msgSend has lots of registers available.
// Cache scan decrements. No end marker needed.
#define CACHE_END_MARKER 0
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}

至此,cache源码分析告一段落!

问题解释及LGPerson例子解释

  • _mask: 是指掩码数据,用于在哈希算法或者哈希冲突算法中计算哈希下标,其中mask 等于capacity - 1,所以例子中刚开始未调用方法时,_mask = 0,调用第一个方法时候capacity大小为4_mask = 4 - 1 = 3,当调用第三个方法时,此时cache容量扩大了,capacity大小为8,_mask = 8 - 1 = 7
  • _occupied: 表示哈希表中 bucket占用大小(即可以理解为当前cachebuckets中已经存储了 bucket_t的的个数),一开始未调用方法_occupied = 0,调用第一个时,增加1,调用第二个时,增加1变为2,此时当调用第三个方法时,由于此时newOccupied = _occupied + 1 = 3 ,占用大于capacity四分之三,所以需要扩容,同时清理之前存储的,及_occupied = 0,然后插入第三那个方法,增加_occupied值。所以得出结论,_occupied即为当前buckets中所占用的bucket_t个数。
  • 因为sel-imp的存储是通过哈希算法计算下标的,其计算的下标有可能已经存储了sel,所以又需要通过哈希冲突算法重新计算哈希下标,所以导致下标是随机的,并不是固定的,也不是按方法调用顺序来存放的。

你可能感兴趣的:(iOS底层 - cache原理分析)