iOS底层之类的cache分析

16,397 阅读6分钟

iOS 全网最新objc4 可调式/编译源码
编译好的源码的下载地址

序言

在前面文章类的结构中,我们分析了bits的结构,isa以及superclass是为指针类型,还剩下一个cache没有分析,cache顾名思义就是缓存相关的,今天就来看一下cache是怎么个原理。

cache的数据结构

先自定义一个类LGPerson,代码实现 image.png

LLDB输出数据结构

LLDB调试输出,查看cache的数据

image.png 有几个关键数据:_bucketsAndMaybeMask_maybeMask_flags_occupied_originalPreoptCache

cache源码数据结构

然后我们再看cache_t的源码结构

image.png 总结

  • _bucketsAndMaybeMask:一个uintptr_t类型的指针;
  • 联合体:一个结构体和preopt_cache_t结构体类型的指针变量_originalPreoptCache
  • _maybeMask: mask_t泛型的变量;
  • _flagsuint16_t类型变量;
  • _occupieduint16_t类型变量
  • preopt_cache_tpreopt_cache_t结构体类型的指针变量;

这里我们还无从知道cache是怎样缓存的数据,以及缓存的是什么数据,是属性还是方法呢?

cache缓存数据类型

既然通过cache_t的数据结构看不出来,那我们就找方法

缓存应该有增删改查等方法,那就从这些方法下手吧。通过阅读源码,我们看到有一个insert方法和copyCacheNolock方法

image.pnginsert方法中,插入的是SELIMP,由此可以看出cache缓存的数据是方法method,然后再看一下insert的实现,找一下SELIMP是缓存在哪里。

cache缓存的存储位置

image.png 这里很明显是一个bucket_t类型的b,调用set方法插入SELIMP以及关联的Class

看一下bucket_t的结构。

image.png 这里我们可以简单总结一下类中cache_t的结构

类结构之cache_t-导出.png

cache缓存数据输出查看

现在我们已经找到了cache缓存的方法是存在bucket_t中,并且bucket_t有成员变量_sel_imp,在insert中是通过方法buckets()获取到的bucket_t,那我们就找到输出一下。

LLDB找到cache缓存数据

cache_t的结构体定义中,正好有buckets()方法,那我们在LLDB中获取到cache的地址变量就可以输出bucket_t

image.png

声明一个LGPerson类型的变量p,并调用对象方法sayHello

image.png 然后我们用LLDB调试输出信息

image.png 我们成功获取到了bucket_t类型的$3,但当我查看$3的内容是缺还是空值。why!!!why!!!why!!!

还是回归到insert源码,看一下到底是怎么插入的缓存吧。

image.png 天呢!漏了一个细节,这里缓存插入的时候是用了hash算法取下标的方式,那我们上面取到的第一个bucket_t的就可能为空值
既然这样,buckets()的存储结构是一个哈希数组,那我们就继续往下面找bucket_t

image.png 这里的_sel中的Value_imp中的Value明显和上面的不一样了,不再是nil0,那我们可以猜测这是一个有效的bucket_t。 找到bucket_t结构体中的方法sel()imp(),输出一下

image.png

image.png Done!!
这里我们成功找到了缓存的方法sayHello,但是我发现在LLDB这样调试很是麻烦,而且还依赖于源码的运行环境,如果有系统升级或者源码有更新,编译不了源码,难道只能GG吗,所以能不能脱离源码编译环境也能搞定上面的步骤呢

脱离源码分析cache

我们的目的是获取cache_t里面的bucket_tcache_t是在objc_class里面,那我们就按照源码objc_class的结构去自定义一个相似的结构体,这样就可以通过NSLog输出获取的内容信息。

typedef uint32_t mask_t// x86_64 & arm64 asm are less efficient with 16-bits
struct lg_bucket_t {
    SEL _sel;
    IMP _imp;
};
struct lg_cache_t {

    struct lg_bucket_t * _buckets;
    mask_t               _maybeMask;
    uint16_t             _flags;
    uint16_t             _occupied;
};
struct lg_class_data_bits_t {
    uintptr_t bits;
};
struct lg_objc_class {
    Class isa;
    Class superclass;
    struct lg_cache_t cache;             // formerly cache pointer and vtable
    struct lg_class_data_bits_t bits;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        LGPerson * p = [LGPerson alloc];
        [p sayHello];
        
        Class pClass = [LGPerson class];
        struct lg_objc_class *lg_class = ( __bridge struct lg_objc_class *)(pClass);
        NSLog(@" - %hu  - %u",lg_class->cache._occupied,lg_class->cache._maybeMask);

        for (int i = 0; i < lg_class->cache._maybeMask; i++) {
            struct lg_bucket_t bucket = lg_class->cache._buckets[i];
            NSLog(@"SEL = %@ --- IMP = %p", NSStringFromSelector(bucket._sel), bucket._imp);
        }
        NSLog(@"Hello, World!");
    }
    return 0;
}

运行上面的代码,查看输出

image.png

成功输出,这里的_occupied为1,_maybeMask为3,我们再调两个方法sayHello_1sayHello_2验证一下。

image.png

image.png 这里发生了蹊跷,_occupied为1,_maybeMask变成了7,而缓存中只有方法sayHello_2,我们调用的sayHellosayHello_1却不在缓存中。既然这样,那就从头捋一遍源码,看看是不是又漏下什么细节了。

cache底层原理分析

对于底层原理分析,就从cache_t的插入方法insert入手

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;
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    if (slowpath(isConstantEmptyCache())) { // 1.判断当前缓存是否为空的
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE; // 1 << 2 = 4
        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);

#endif // !DEBUG_TASK_THREADS

}

整个代码流程我们按调用次数分布解析

第一次调用方法insert

image.png 第一次插入缓存时

  1. _occupied的值为0,所以newOccupied为1;
  2. capacity()取的是_maybeMask的值,所以oldCapacitycapacity值都为0;
  3. isConstantEmptyCache判断当前缓存是否为空,条件成立,进入if语句;
  4. capacity值为0,赋值为INIT_CACHE_SIZEINIT_CACHE_SIZE = 1 << 2值为4;
  5. 调用reallocate开辟内存;

image.pngreallocate方法中,开辟新内存,然后调用setBucketsAndMask方法,使cache_t中的成员变量_bucketsAndMaybeMask_maybeMask_occupied做关联

image.png 之后是再开辟的缓存空间中存入SELIMP

image.png 在存入SELIMP的方法中,有对IMP进行编码,实际上存入的是编码后的newImp

image.pngimp编码的源代码

image.png

image.png 我们用到的CACHE_IMP_ENCODING情况为CACHE_IMP_ENCODING_ISA_XOR,所以上面的编码算法是imp & cls

我们知道了第一次调用方法,会开辟空间为4缓存空间,当我们调用更多方法的时候,应该在什么时候扩容呢?

四分之三扩容

当我们不是第一次调用方法时,就会进入一个剩余空间容量判断

image.png

  • newOccupied:进入缓存的第几个方法;
  • CACHE_END_MARKER:宏定义值为1;
  • cache_fill_ratio(capacity): capacity * 3 / 4容量的3/4值;

这里我们知道当新的调用方法进入缓存时

  1. 如果不满足扩容条件,就会继续往开辟的缓存空间插入一条缓存数据。比如:调用sayHello_1时,newOccupied为2,capacity为4, 2 + 1 <= 4 *3 / 4的条件满足。
  2. 如果到达扩容条件,就会先开辟2倍的新内存,然后再插入新的缓存数据。比如:调用sayHello_2时,newOccupied为3,capacity为4, 3 + 1 <= 4 *3 / 4的条件不满足,就会进入else语句,开辟2倍容量的新内存。


在开辟新内存中调用方法reallocate时,传入的最后一个参数freeOldtrue,会把旧的缓存空间清理释放掉,不会copy缓存数据到新的缓存空间,这也是为什么调用sayHello_2时,输出的只有sayHello_2

image.png

总结

关于objc_class中的cache的原理分析,我们先是查看cache_t数据结构,根据数据结构我们无法知道其工作原理,然后我们通过结构体中的方法去找线索,最后锁定insert方法,根据insert方法来大致了解整个缓存插入的流程。
cache_t的工作原理流程图:

类结构之cache_t-导出(1).png