iOS底层探究--------cache分析

463 阅读11分钟

前言

在前面的文章中iOS底层探究--------类的原理分析(下),我们探索了isa、superclass、bits,那么整个类结构(如下图所示),还剩下cache没有进行探索了。那么今天,就来把这块给补齐。 未命名文件-2_gaitubao_200x226.png

cache从词面上来看,是缓存的意思。那么这个cache的底层是怎样的了,我们接下来进行探索。

从上篇文章中,我们获取bits是通过内存平移得到的,那么我们获取cache是不是也可以用这个方法,探索cache是什么?他的基本数据结构是什么?

cache的流程图

未命名文件-5.png

1、探索cache的数据结构

1.1、通过lldb调试,初探cache内存结构

探索的用的代码还是objc源码,创建一个TestPerson类,然后在main.m里面获取其Class,再通过lldb调试查看,如下图所示: 0009BE8F-1781-48C0-BDEA-D10D7356444A.png D25217B3-7674-426B-93FE-0E29536CE5A1.png

那么接下来,就是lldb的调试结果了: 3519CFF0-9862-4592-9096-DACE8A49127A.png

类结构里面的包含的内容是:isa(8字节)superclass(8字节)cache(16字节)bits。根据源码,其中,cachecache_t结构体,进入到cache_t结构体里面(源码代码):

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask;
#if __LP64__
            uint16_t                   _flags;
#endif
            uint16_t                   _occupied;
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;
    };
    
   //省略代码。。。
   
    //cache的平移
    static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
    static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << maskShift) - 1;
    
    //省略代码。。。。
      
    //bucket_t的清空操作
    static bucket_t *emptyBuckets();
    
    //省略代码。。。。
    
    //插入SEL sel, IMP imp, id receiver的操作
    void insert(SEL sel, IMP imp, id receiver);

    //省略代码。。。。
    }

看到源码,再对比下刚刚lldb调试中,$3所打印的结果,是不是能发现,就是cache里面的内容。到这里,就有疑问了,既然cache是缓存,那么缓存的也应该是变量和方法,应该是与SEL或者IMP相关才对,但是,通过源码,却没有看到与之相关的。根据打印的结果,_bucketsAndMaybeMask和_originalPreoptCache所包含的value,分别都是有值的。

既然都有值,那么就需要对应的去看_bucketsAndMaybeMask_originalPreoptCache各自的功能方法了。

cache能够进行缓存,那么与之对应的,就应该有增、删、改、查的功能,读写一定少不了。回到源码中,除了_bucketsAndMaybeMask_originalPreoptCache,还有很多是对cache的地址平移的操作(如:((uintptr_t)1 << (64 - maskShift)) - 1);bucket_t的清空与创建;甚至还有插入的操作 insert(SEL sel, IMP imp, id receiver)。那么我们需要分析出,哪个才是关键点。

1.2、cache里面缓存的方法是在bucket_t里面

既然有插入,那么就有读写,那么进入到插入的操作里面看看具体执行了什么?

void cache_t::insert(SEL sel, IMP imp, id receiver) {
   //对各种不符合条件的判断报出错误码
      
   //省略代码。。。。
       
    //通过buckets数组来判断需要插入的内容情况
    bucket_t *b = buckets();  
    mask_t m = capacity - 1;
    mask_t begin = cache_hash(sel, m);
    mask_t i = begin;

   //省略代码。。。。

通过快速浏览,可以知道,插入是需要buckets数组的,而接收者是bucket_t,那么这个关键点,算是找到了。那么就需要查看bucket_t的具体实现了。

struct bucket_t {
private:
x86_64.
#if __arm64__                 //架构1 arm64
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else                         //除了架构1其他的架构
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif

//省略代码。。。。

进入到bucket_t里面,就很明了了,其本身就是个数组buckets,里面就是很多组impsel的缓存地方,_imp_sel一一对应,如下如所示: 未命名文件-4.png

  • 综上分析,我们就能得到一个大概的数据结构关系走势图了: 未命名文件.png

2、cache底层lldb分析

2.1、lldb深入探究

理清这个结构图之后,我们就需要到代码中去验证了,通过lldb的调试,看看里面是不是存储了方法,通过前面lldb调试到的$3接着往下查找_bucketsAndMaybeMask所储存的内容: 截图.png 最后却是报了error的错误。那么说明_bucketsAndMaybeMask查找的路线走不通,接着换_originalPreoptCache查找试试: 截图.png 发现同样走不通。

我们发现都走不通的时候,就直接去源码中去寻找有没有相关的方法了。再结合刚刚分析的cache的大概数据结构图,就能锁定bucket_t相关的方法了。既然selimp的组合是存储在buckets数组里面,那么就到这个数组里面去查找:

//省略代码。。。。
struct bucket_t *buckets() const;
//省略代码。。。

可以查看buckets()存储情况,再次进行lldb调试,还有就是需要先执行TestPerson里面的saySomething方法,才会有缓存,所以在最开始的时候,执行saySomething方法: 91BFD7AA-A8AB-421F-BE01-993BC758B457.png

这次调试的结果,能够得到_sel_imp的信息了,虽然他们的value0,但是方向上是正确的了。但是我们发现在$3中,_occupied的值是为0的,这个是什么原因了?因为方法没有调用,所以没有开始缓存。所以在调试之前,需要先调用。

根据上面的调试结果,发现$5找到的地址,存储的值是空的,因为_sel_imp对应的值都是0,我们明明是进入到buckets数组里面去取的值。之所以会出现这样的情况,是因为苹果系统在将_sel_imp组合存入buckets数组时,做了哈希处理。

用了个哈希链表。什么事哈希链表了?先看看接下来lldb调试的步骤,通过 p $3.buckets()[2],就是去buckets数组的第2个角标的值: 6F810849-BE05-46CB-B0FF-6795B8230E6A.png 这就把方法给找到了。

2.2、Hash链表

为什了是 p $3.buckets()[2]了?

这个就得了解什么是哈希链表了?

  • 计算机的存储方式有两种,一种是顺序存储,另一种是链式存储。 哈希链表,是采用数组和链表相结合的数据结构,比如,有一组关键数字{12,13,25,23,38,34,6,84,91}Hash 表长为14Hash 函数为 address(key) = key % 11;还有就是一个array[13]的数组结构。在这组关键数字中,通过哈希函数算出12、23、34的哈希值是一样的,都为1,那么,12、23、34就以链表结构的形式,存储在数组1号位的元素中,如下图所示: 4146031-a20c9a622d146c29.png

其他的数字处理方法是一样的,根据哈希函数算出的哈希值,再把对应的数字存储进入对应的号位里面。

2.3、验证苹果系统对buckets数组做了Hash处理

那么回归到cache的分析上来,那么方法的存储号位,就不一定存储在0号位,也可能是请他号位。就比如找到的这个方法,就在2号位找到的。

现在我们要想知道我们刚刚找到的这个方法到底是什么?那么就需要到bucket_t里面找是否有相应的方法。看源码:

inline SEL sel() const { return _sel.load(memory_order_relaxed); }

是有个sel()方法输出的。那么,在lldb的调试里面,加上sel(),就能打印出方法名了: 03C41E7D-FD81-4BA3-90C2-DAF41402E38B.png 这样就能找到我们需要找的方法了。

那么相应的就要找_imp的输出结果了,那么还是要在源码中找方法:

inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls)

根据源码,需要传入一个basecls,由于我们不知道base是什么,所以就传了nilcls就是TestPerson类了,传pClass,就能得到结果: 5860B722-3260-4DA2-A61F-3AFE84ACEF59.png

接下来,用代码来分析。

3、脱离源码分析cache

3.1、仿照源码,简化版实现类结构体cache结构体

之所以要用代码分析:

  • 第一个原因:假如下载到陌生的代码或者是无法直接运行调试的源码时,会更方便一些;
  • 第二个原因:当在使用lldb时,增减一些属性、方法,就需要再次执行比较多的重复步骤,比较繁琐;
  • 第三个原因:小规模取样的方式,会让你对底层更加清晰。

我们就以根据源码,自定义一个类似的类的数据结构,来查看cache里面的情况。

那么就建立一个project工程,在工程里面,创建一个TestPerson类,代码如下:

//TestTestPerson类
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface TestPerson : NSObject

- (void)say1;
- (void)say2;
- (void)say3;
- (void)say4;
- (void)say5;
- (void)say6;
- (void)say7;
+ (void)sayHappy;
@end


#import "TestPerson.h"

@implementation TestPerson

- (void)say1{
    NSLog(@"TestPerson say : %s",__func__);
}
- (void)say2{
    NSLog(@"TestPerson say : %s",__func__);
}
- (void)say3{
    NSLog(@"TestPerson say : %s",__func__);
}
- (void)say4{
    NSLog(@"TestPerson say : %s",__func__);
}
- (void)say5{
    NSLog(@"TestPerson say : %s",__func__);
}
- (void)say6{
    NSLog(@"TestPerson say : %s",__func__);
}
- (void)say7{
    NSLog(@"TestPerson say : %s",__func__);
}

+ (void)sayHappy{
    NSLog(@"TestPerson say : %s",__func__);
}
@end

//main.m文件
#import <Foundation/Foundation.h>
#import "TestPerson.h"
#import <objc/runtime.h>

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

//bucket_t结构体
struct sc_bucket_t {
    SEL _sel;
    IMP _imp;
};

//cache_t 结构体
struct sc_cache_t {
    //uintptr_t _bucketsAndMaybeMask 换成下面这行(通过源码如何获取bucket(源码): uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);)
    struct sc_bucket_t *_bukets; // 8
    mask_t    _maybeMask; // 4
    uint16_t  _flags;  // 2
    uint16_t  _occupied; // 2
};

//bits结构体
struct sc_class_data_bits_t {
    uintptr_t bits;
};

//类的结构体
struct sc_objc_class {
    Class isa;
    Class superclass;
    struct sc_cache_t cache;             // formerly cache pointer and vtable
    struct sc_class_data_bits_t bits;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        TestPerson *p  = [TestPerson alloc];
        Class pClass   = p.class;
        
        struct sc_objc_class *sc_class = (__bridge struct sc_objc_class *)(pClass);
        NSLog(@"%hu - %u",sc_class->cache._occupied,sc_class->cache._maybeMask);
    }
    return 0;
}

现在,不调用TestPerson类里面的实例方法和类方法,运行下,查看结果: 0EA7FB62-0D80-47EC-9198-873F765F2EFB.png

现在再调用TestPersonsay1方法,[p say1]; 2B857A03-C5A4-494A-BC4A-2D2A063A27E5.png

接着,我们可以再增加say2、say3、say4、say5、say6、say7等实例方法,看打印结果: 3C110625-6319-41AC-86B6-5B5555DA5BEF.png 有点当机了,这是啥情况啊?

3.2、类里面的方法不是存在cache里面

3.2.1、类里面的方法存储在另外新开辟内存里

要寻找答案,就需要进入到源码里面去了。 92E51A59-5132-4455-BEAD-FB46244E5D87.png 在结构体cache_t里面,他的其中一个地址存储在_bucketsAndMaybeMask中,而_bucketsAndMaybeMask是由两部分构成:bucketsmask,也就是说里面相当于存储了这么两个数据。那该怎么去获取这两个数据了?

有个切入点,就是我们前面提到的缓存方法的插入。

我们知道cache就是缓存,那么有缓存,就会有插入,也就会有写的过程。那么就能在cache_t的结构体里面,找到插入的方法: DB858B7B-8D73-4AE9-9262-EDF64ED901FA.png

从这个插入的方法来看,插入的参数有sel、imp、还有receiver消息接收者(谁调用了这个消息)。那么在进入这个插入方法里面去看实现:

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;
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    if (slowpath(isConstantEmptyCache())) {
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE;
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }
    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.
    }
#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 {
        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;
    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();
            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);
}

由源码可知,首先定义一个newOccupied,他的值是occupied() + 1,而occupied()方法里面,就只有一句return _occupied,那么occupied()初始值就为0,所以newOccupied的初始值为1;根据源码,接下来就是有无缓存的判断,当编译第一次进来的时候,缓存是为空的,所以就进入下面这个判断中 :

if (slowpath(isConstantEmptyCache())) {
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE;
        //INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2),而INIT_CACHE_SIZE_LOG2 = 2,所以是向左平移2。
        //0x001,偏移2位,就是0x100,所以INIT_CACHE_SIZE的值为4,
        
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }

一开始没有capacity,所以直接去获取INIT_CACHE_SIZE的值4,那么capacity = 4。紧接着就是reallocate()开辟操作,进入reallocate()实现方法里面:

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
    //原有的buckets(),赋值给旧的oldBuckets
    bucket_t *oldBuckets = buckets();
   
    //开辟新的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);

    //cache_t的第一个地址就是_bucketsAndMaybeMask,此时是第一次编译,所以插入的是一个空的容器进来,给接下来将要被插入的内容一个装填的容器
    setBucketsAndMask(newBuckets, newCapacity - 1);
    
    if (freeOld) {
        collect_free(oldBuckets, oldCapacity);
    }
}

我们先看下在buckets的开辟过程,进入allocateBuckets()方法里面去,查看实现:

size_t cache_t::bytesForCapacity(uint32_t cap)
{
    return sizeof(bucket_t) * cap;//bucket_t大小
}

#if CACHE_END_MARKER  //macOS 模拟器

bucket_t *cache_t::endMarker(struct bucket_t *b, uint32_t cap)
{
//(首地址+开辟的内存)-1,相当于获取最后一个位置的地址
    return (bucket_t *)((uintptr_t)b + bytesForCapacity(cap)) - 1;
}

bucket_t *cache_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(bytesForCapacity(newCapacity), 1);//开辟新的bucket_t

    bucket_t *end = endMarker(newBuckets, newCapacity);//获取数组中最后一个bucket的地址

#if __arm__
    // End marker's sel is 1 and imp points BEFORE the first bucket.
    // This saves an instruction in objc_msgSend.
    end->set<NotAtomic, Raw>(newBuckets, (SEL)(uintptr_t)1, (IMP)(newBuckets - 1), nil);
#else
    // End marker's sel is 1 and imp points to the first bucket.
    //给最后一个位置的bucket赋值  sel = 1,imp = 新创建的bucket地址,
    end->set<NotAtomic, Raw>(newBuckets, (SEL)(uintptr_t)1, (IMP)newBuckets, nil);
#endif
    
    if (PrintCaches) recordNewCache(newCapacity);//记录新的缓存

    return newBuckets;
}

到此,就是newBuckets的开辟过程。

接着再进入到setBucketsAndMask()方法的实现中去:

void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
#ifdef __arm__  //
    // ensure other threads see buckets contents before buckets pointer
    mega_barrier();
    //这里开始把newBuckets存储到_bucketsAndMaybeMask里面去
    _bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_relaxed);

    // ensure other threads see new buckets before new mask
    //设置内存屏障,防止多个线程同时访问
    mega_barrier();
    
    _maybeMask.store(newMask, memory_order_relaxed);
    //此时occupied值为0
    _occupied = 0;
#elif __x86_64__ || i386 //macOS 和 模拟器
    // ensure other threads see buckets contents before buckets pointer
    //这里开始把newBuckets存储到_bucketsAndMaybeMask里面去
    _bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_release);

    // ensure other threads see new buckets before new mask
    //把_maybeMask写入数据
    _maybeMask.store(newMask, memory_order_release);
    _occupied = 0;//新开辟的bucket里面,没有缓存任何方法,占位计数为0
#else
#error Don't know how to do setBucketsAndMask on this architecture.
#endif
}

3.3.2、cache通过存储的地址到新开辟的空间读写方法

  • 通过源码的分析,就知道,在cache里面,只是存储指针地址,而被调用的方法,却存在另外开辟的一片空间内(源码分析里,所说的容器)。而这个新开辟存储方法的空间,对方法的存储方式是:每一个方法的selimp存储在一个bucket里面,而bucket又存储在新开辟的内存里面,这就是buckets数组的来源。当读取方法的时候,就只要通过cache里面的指针地址,到新开辟的空间里面找到对应的bucket来获取方法。

  • 这样的好处就是,无论存入多少方法,cache的内存不会随着方法的数量增多而无限增大,因为cache里面存的只是指针地址,每个指针地址的大小,只有8字节。这就是苹果对内存的优化。下面是图解: 8940E913-1A3C-4BEC-8780-24A498B59E2D.png

3.3.3、buckets数组存储通过Hash处理

接着再返回到void cache_t::insert(SEL sel, IMP imp, id receiver)这个插入方法的实现里面。当开辟新的存储空间之后:

//获取buckets
bucket_t *b = buckets();
//刚刚在开辟空间时,计算出来,得到的capacity为4,这里再减1,那么m = 3
mask_t m = capacity - 1;
//因为buckets是数组,起始分配的空间是4,那么第一个方法存放的位置,是通过哈希计算出一个地址,就是开始的缓存位置。
mask_t begin = cache_hash(sel, m);
mask_t i = begin;

当计算出mask_t m = 3时,就能解释,之前调用TestPerson类的say1方法,打印出的结果是1-3了,_occupied = 1_maybeMask = 3

当得到buckets数组后,开始存储的起始位置,就需要通过哈希函数进行计算了,进入到cache_hash()方法里面:

static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
    value ^= value >> 7;
#endif
    return (mask_t)(value & mask);
}

当计算出这哈希值后,就开始进行缓存了。接着就进行一个do-while循环,查找空位置。

do {
        if (fastpath(b[i].sel() == 0)) {//当找到空位置
            incrementOccupied();//这个方法的实现是:_occupied++;就是占位计数+1,表示这个bucket存储了方法.
            b[i].set<Atomic, Encoded>(b, sel, imp, cls());//在buckets数组的i位置,插入对应的参数,把sel和imp写入进来
            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));

存储时,为了防止哈希冲突(就是方法不一样,但是下标一样),所以再次进行哈希一次cache_next(),看下面源码实现,分为不同架构:

#if CACHE_END_MARKER // __arm__ || __x86_64__ || __i386__
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}
#elif __arm64__   //真机
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}

那么这就是第一次执行插入方法的过程了。

3.3.4、根据类里面的方法的增减,会重新开辟内存空间

接下来的方法插入时:

  • 当缓存的方法所占容量小于目前总容量3/4(capacity * 3 / 4),就执行判断:else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))),而cache_fill_ratio(capacity) = capacity * 3 / 4
static inline mask_t cache_fill_ratio(mask_t capacity) {
    return capacity * 3 / 4;
}
  • 当满存时。苹果支持容量满存,在满存的情况下,不做任何处理
#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.
    }
  • 当缓存的方法所占容量超出目前总容量的3/4(capacity * 3 / 4),那么就需要扩容了capacity * 2,执行else判断。刚刚算出来,capacity4,两倍扩容就是8,但是mask_t m = capacity - 1,所以,m = 7else判断源码:
else {
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        reallocate(oldCapacity, capacity, true);
    }

如何进行扩容的了?就得进入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);

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

会重新开辟一个新的空间,因为传入进来的freeOldtrue,所以,也会把旧的空间给释放掉,进入collect_free()方法里面:

void cache_t::collect_free(bucket_t *data, mask_t capacity)
{
#if CONFIG_USE_CACHE_LOCK
    cacheUpdateLock.assertLocked();
#else
    runtimeLock.assertLocked();
#endif

    if (PrintCaches) recordDeadCache(capacity);
//把垃圾站进行清空回收
    _garbage_make_room ();//创建回收站
    garbage_byte_size += cache_t::bytesForCapacity(capacity);//获取开辟的内存大小
    garbage_refs[garbage_count++] = data;//将bucket的地址往后移
    cache_t::collectNolock(false);//释放回收内存
}

所以,就解释了上面,在工程里面,第二次打印时,为什么只有say7的地址,而其他的6个地址为0的原因,也间接的可以看出,扩容的操作进行了两次,第一次是在4时,进行扩容到8,当8也不能满足的时候,扩容到16,所以,才有1-15这的结果。那为什么say7还在了?

逐步分析下,根据刚刚源码分析的结果,当say7写入进来时,系统发现内存不够了,那么就会进行扩容操作,旧的空间内存是存储的say1say6的方法,此时扩容,把say7存入新的空间内存里面,然后把旧的空间内存给释放掉,所以,当再去打印say1say6的地址时,是为0x0的。

  • 苹果系统之所以这么做,是因为,原有开辟内存是不能修改的。只能是重新创造一个新的内存来取代原来的内存。同时,数组的平移是十分耗费性能的。当开辟新的内存时,苹果系统会把旧内存里面存储的数据视为不活跃数据,是可以进行剔除的。 F23DB4BF-31E6-4147-AB4C-F937B4175DA0.png 如上图所示,在存入say3时,已经把原有内存释放了,当再次执行say1、say2时: 2A16D982-661C-4D44-801C-EB428C2159C7.png 因为哈希函数计算的原因,方法存储之间可能存在空隔。

4、cache在插入方法之前做了什么?

当有方法将要被缓存时,cache执行的流程,但是,是谁发起的缓存消息了?接下来通过实例方法来探究,当TestPerson类调用- (void)saySomething方法时,是怎样执行到insert()上去的。

4.1、探究insert()之前执行流程

既然我们要探究cache在插入方法前做了哪些事情,那么我们是不是的在cache_t结构体里面,插入的方法上,进行跟踪。那么就在insert()方法的实现里面打上断点。如下图: 6DA6340B-DDA0-44F1-95F6-2F454061A439.png 当运行到断点上时,可以通过lldb,输入bt指令查看堆栈消息(其实,在工程的左边,也能看到): 0DAE51AC-C813-47AC-BA65-0058E8E8D4A2.png 根据堆栈消息,我们就知道在执行插入insert()方法之前,是执行的方法流程:从main()函数的autoreleasepool --> _objc_msgSend_uncached --> lookUpImpOrForward --> log_and_fill_cache --> cache_t::insert.

4.2、通过汇编查看信息

刚刚通过堆栈信息,知道了调用实例方法[p saySomething],会有从_objc_msgSend_uncached执行到cache_t::insert这样的部分流程,但是从开始调用方法,到_objc_msgSend_uncached这部分流程,还不知道,那么我们就可以用终极手段--汇编9BF448A7-1B1D-4E7C-93C1-1C580781A540.png 执行[p saySomething]后,是走的objc_msgSend()方法。接着跟踪断点(里面内容太多,分两段截图): 9A013A15-AF10-4FAD-BFFD-A0CDBC21CCBF.png 44BC851D-C965-4035-B5A4-E5F8FF57E3FA.pngobjc_msgSend()方法里面,再执行了_objc_msgSend_uncached()方法。到了这里,就与前面的步骤连接上了。

  • 调用insert()方法流程:[p saySomething]底层实现 objc_msgSend --> _objc_msgSend_uncached --> lookUpImpOrForward --> log_and_fill_cache --> cache_t::insert

那么insert()的流程图为: 未命名文件-4.png

okcache的分析,到此全部完成了。