iOS底层原理09:cache_t底层原理探索

333 阅读5分钟

前言

这篇文章将对类结构中cache_t的分析。

cache_t的结构

cache_t源码。

struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED//macOS、模拟器
    // explicit_atomic 显示原子性,目的是为了能够 保证 增删改查时 线程的安全性
    //等价于 struct bucket_t * _buckets;
    //_buckets 中放的是 sel imp
    //_buckets的读取 有提供相应名称的方法 buckets()
    explicit_atomic<struct bucket_t *> _buckets;
    explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 //64位真机
    explicit_atomic<uintptr_t> _maskAndBuckets;//写在一起的目的是为了优化
    mask_t _mask_unused;
    
    //以下都是掩码,即面具 -- 类似于isa的掩码,即位域
    // 此处省略....
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 //非64位 真机
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;

    //以下都是掩码,即面具 -- 类似于isa的掩码,即位域
    // 此处省略....
#else
#error Unknown cache mask storage type.
#endif
    
#if __LP64__
    uint16_t _flags;
#endif
    uint16_t _occupied;

    //方法省略.....
}
  • 从源码中可以看出,在macOS、64位真机和非64位真机环境下有不同的处理。
  • CACHE_MASK_STORAGE_OUTLINED macOS、模拟器
  • CACHE_MASK_STORAGE_HIGH_16 64位真机
  • CACHE_MASK_STORAGE_LOW_4 非64位 真机
  • explicit_atomic显示原子性,为线程的安全。
  • 其中在真机环境下,mask和bucket是写在一起,目的是为了优化。

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(SEL newSel, Class cls) const {
        return (uintptr_t)&_imp ^ (uintptr_t)newSel ^ (uintptr_t)cls;
    }
  • bucket_t分为两个版本,真机非真机,不同的区别在于selimp的顺序不同。
  • 从上面的源码中,也可以清晰看出,cache_t缓存了impsel,具体存储在bucket_t中。

获取_buckets

  • 既然我们知道impsel存储在_buckets中,那么如何获取呢?
  • 定义一个Person类继承NSobject。
@interface Person : NSObject{
    NSString *hobby;
}

@property (nonatomic,copy) NSString *name;

- (void)eat;
- (void)say;
+ (void)run;

@end

@implementation Person
- (void)eat{
    NSLog(@"eat something");
}
-(void)say{
    NSLog(@"say hi");
}
+ (void)run{
    NSLog(@"run run");
}
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Person *person = [Person alloc];
        Class pClass   = object_getClass(person);

        [person eat];
        [person say];
        [person say];
    }
    return 0;
}
  • [person eat]之前打断点,通过lldb调试工具,查看person的类的内存结构,lldb执行p/x pClass获取首地址。
(lldb) p/x pClass
(Class) $0 = 0x00000001000033a8 Person
  • 前面文章中,我们知道了类的内存布局,是以 isasuperclasscache_tclass_data_bits_t 顺序排布的,其中 isasuperclass 分别占用8个字节,所以 cache_t 的位置应该是从类的首地址偏移16字节的位置,所以我们取 0x00000001000033a8 偏移 16字节,即 0x00000001000033b8 的地址。
  • 执行p (cache_t *)0x00000001000033b8获取cache_t的首地址。
(lldb) p (cache_t *)0x00000001000033b8
(cache_t *) $1 = 0x00000001000033b8
  • 执行p *$1,打印cache
(lldb) p *$1
(cache_t) $2 = {
  _buckets = {
    std::__1::atomic<bucket_t *> = 0x000000010032f410 {
      _sel = {
        std::__1::atomic<objc_selector *> = (null)
      }
      _imp = {
        std::__1::atomic<unsigned long> = 0
      }
    }
  }
  _mask = {
    std::__1::atomic<unsigned int> = 0
  }
  _flags = 32804
  _occupied = 0
}
(lldb) 
  • 此时没有调用方法,_sel = null_imp = 0_occupied = 0
  • 我们打个断点在[person say]处,上面[person eat]的断点执行下一步step over
  • 继续执行p *$1
(lldb) p *$1
(cache_t) $3 = {
  _buckets = {
    std::__1::atomic<bucket_t *> = 0x0000000100694ab0 {
      _sel = {
        std::__1::atomic<objc_selector *> = ""
      }
      _imp = {
        std::__1::atomic<unsigned long> = 10424
      }
    }
  }
  _mask = {
    std::__1::atomic<unsigned int> = 3
  }
  _flags = 32804
  _occupied = 1
}
  • 此时的,_sel_imp都有值,_occupied = 1。但要怎么获取_buckets,于是看了看cache_t的源码,终于有了新的发现。
  • 于是很愉快的执行buckets()去获取,p $3.buckets()
(lldb) p $3.buckets()
(bucket_t *) $4 = 0x0000000100694ab0
  • 执行p *$4打印_buckets信息。
(lldb) p *$4
(bucket_t) $5 = {
  _sel = {
    std::__1::atomic<objc_selector *> = ""
  }
  _imp = {
    std::__1::atomic<unsigned long> = 10424
  }
}
  • 那么如何才能获得_sel_imp呢,于是才看_buckets源码,在源码中看到了,获取_sel_imp的方法。
  • 执行 p $4.sel(),获取sel
(lldb) p $5.sel()
(SEL) $6 = "eat"
  • 执行 p $5.imp(pClass),获取imp
(lldb) p $5.imp(pClass)
(IMP) $7 = 0x0000000100001b10 (KCObjc`-[Person eat])

获取_buckets小结

  • 1、cache的获取,需要通过pClass的首地址平移16字节,即首地址+0x10获取cache的地址
  • 2、cache_t结构体中提供了buckets()获取_buckets
  • 3、_buckets结构体中,通过sel()imp(pClass)分别获取selimp
  • 4、没有调用方法时,cache是没有缓存的,调用方法后,cache中就有了一个缓存,即调用方法后就会缓存在cache中。

cache_t的结构图

cache怎么存?

  • cache_t源码中,找到了引起变化的函数,incrementOccupied()函数。
void cache_t::incrementOccupied() 
{
    _occupied++;  //occupied自增
}
  • 全局搜索incrementOccupied()函数,发现只在cache_t的insert方法有调用
  • 全局搜索insert(函数,发现cache_fill才符合调用
  • 全局搜索cache_fill,消息发送之后,会获取imp,接着才调用cache_fill方法写入缓存,即objc_msgSend->cache_getImp->cache_fill

insert源码分析

  • insert()中,其源码实现如下
  • cache->insert 函数大致做了3件事

1、初始化缓存空间

2、判断是否需要扩容,如果需要,以原始空间的2倍扩容,重新分配空间,释放已有缓存信息

3、根据散列表中是否已有该方法的缓存情况插入缓存

1、初始化缓存空间

  • 如果 occupied()为0, 并且buckets中无缓存内容 , 则开辟 4 个存储空间大小 为默认初始值。(4 来自于if (!capacity) capacity = INIT_CACHE_SIZE
    INIT_CACHE_SIZE_LOG2 = 2,
    INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2),
    // 1 << 2  = 4
  • 调用reallocate(oldCapacity, capacity, /* freeOld */false); 分配空间。
ALWAYS_INLINE
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
    bucket_t *oldBuckets = buckets();
    // 开辟空间
    bucket_t *newBuckets = allocateBuckets(newCapacity);

    ASSERT(newCapacity > 0);
    ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
    
    setBucketsAndMask(newBuckets, newCapacity - 1);
    
    // 释放旧的缓存信息
    if (freeOld) {   
        cache_collect_free(oldBuckets, oldCapacity);
    }
}

  • 调用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<NotAtomic, Raw>((SEL)(uintptr_t)1, (IMP)(newBuckets - 1), nil);
#else
    // End marker's sel is 1 and imp points to the first bucket.
    end->set<NotAtomic, Raw>((SEL)(uintptr_t)1, (IMP)newBuckets, nil);
#endif
    
    if (PrintCaches) recordNewCache(newCapacity);
    
    return newBuckets;
}

reallocate函数中 通过 allocateBuckets 函数的 calloc 向系统申请 newCapacity 大小的空间; 并且通过 setBucketsAndMask 设置 bucketsmask,其中 mask 更新为 新申请的总空间大小 - 1 (capacity - 1);

2、判断空间是否足够

  • 如果空间不足, 扩容到原空间大小的2倍值,并重新分配空间大小 并释放已存储的缓存,插入新缓存。
 //  arm64下 如果 newOccupied <= 容量的4分之3,存储空间还足够,不需额外处理
    else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) {
        // Cache is less than 3/4 full. Use it as-is.
    }
    // 如果超过 4分之3
    else {
        // 扩容为原空间的 2倍大小
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;  //  最大不能 超出 1<< 16
        }
        reallocate(oldCapacity, capacity, true); // 重新分配空间   存储新的数据,抹除已有缓存
    }

3、插入缓存

 /**
     3注: 以下为插入缓存的过程
     遍历 buckets()内容,如果在缓存中找到了 传入的方法,直接退出
     如果在缓存中没有找到 传入的方法 将_occupied ++;,并且将方法存入缓存
     如果遇到hash冲突, cache_t查找下一个 直到回到begin 全部查找结束
     */
    
    // 获取散列表
    bucket_t *b = buckets();
    // 获取散列表大小 - 1
    mask_t m = capacity - 1;
    // 通过cache_hash函数【begin  = sel & m】计算出key值 k 对应的 index值
    // begin,用来记录查询起始索引
    mask_t begin = cache_hash(sel, m);
    // begin 赋值给 i,用于切换索引
    mask_t i = begin;
    
    do {
        if (fastpath(b[i].sel() == 0)) {  // 如果没有找到缓存的方法
            incrementOccupied(); //   _occupied ++;
            b[i].set<Atomic, Encoded>(sel, imp, cls); // 缓存实例方法
            return;
        }
        if (b[i].sel() == sel) {  // 如果找到需要缓存的方法,什么都不做,并退出循环
            
            return;
        }
    } while (fastpath((i = cache_next(i, m)) != begin));
    // 当出现hash碰撞 cache_t查找下一个 直到回到begin 全部查找结束
    
    cache_t::bad_cache(receiver, (SEL)sel, cls);

begin 作为 散列表 的初始查询下标,是经过 sel & mask 计算而来的

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

do ... while 循环的条件是 (i = cache_next(i, m) != begin ,判断不等于初始下标值 begin 是为了将散列表中的数据全部遍历结束,而cache_next( ) 是为了解决哈希冲突而进行的二次哈希。

#define CACHE_END_MARKER 1
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}

insert流程图