iOS6 底层之cache分析

294 阅读10分钟

一,cache源码分析

  • cache_t源码

//源码338-550行这里只提供核心部分
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;
    };
}

  • _bucketsAndMaybeMask变量uintptr_t8字节isa_t中的bits类似,也是一个指针类型里面存放地址
  • 联合体里有一个结构体和一个结构体指针_originalPreoptCache
  • 结构体中有三个成员变量 _maybeMask_flags_occupied__LP64__指的是UnixUnix类系统
  • _originalPreoptCache和结构体是互斥的,_originalPreoptCache初始时候的缓存,现在探究类中的缓存,这个变量基本不会用到
  • cache_t提供了公用的方法去获取值,以及根据不同的架构系统去获取maskbuckets的掩码
    public:
    void incrementOccupied();
    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
    void reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld);
    unsigned capacity() const;
    struct bucket_t *buckets() const;
    Class cls() const;
    void insert(SEL sel, IMP imp, id receiver);
  • cache_tbuckets(),这个类似于class_data_bits_t里面的提供的methods(),都是通过方法获取值。查看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
//其他代码保留
}
  • 从源码我们可以看到,我们缓存里面存放的是sel 和 imp。

二,LLDB调试探究


//新建类
@interface NBPerson : NSObject{
    double height;
}

-(void)sayHello;
-(void)sayHello1;
-(void)sayHello2;

+(void)sayNB;

@end

@implementation NBPerson

-(void)sayHello{
    NSLog(@"hello----");
}

-(void)sayHello1{
    NSLog(@"hello----1");
}

-(void)sayHello2{
    NSLog(@"hello----2");
}

+(void)sayNB{
    NSLog(@"NB----");
}


@end

  • 断点运行

截屏2021-08-06 下午4.01.27.png

//类地址+0x10为cache_t地址
//通过cache_t地址查看cahce_t

  • 调试查看cache_t内部结构
(lldb) p/x [NBPerson class]
(Class) $0 = 0x0000000100008188 NBPerson
(lldb) p (cache_t*)(0x0000000100008188 +0x10)
(cache_t *) $1 = 0x0000000100008198
(lldb) p *$1
(cache_t) $2 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 4298515296
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 0
        }
      }
      _flags = 32792
      _occupied = 0
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0000801800000000
      }
    }
  }
}
(lldb) 
  • 源码第454行代码我们看到buckets

截屏2021-08-05 下午5.01.42.png


cache_t中的方法buckets()指向的是一块内存的首地址,也是第一个bucket的地址

  • 那么我们接着调试
(lldb) p [nb sayHello]
2021-08-06 16:16:54.002681+0800 KCObjcBuild[52478:817482] hello----
(lldb) p *$1
(cache_t) $3 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 4302714160
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 7
        }
      }
      _flags = 32792
      _occupied = 1
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0001801800000007
      }
    }
  }
}

(lldb) p $3.buckets()[0].sel()
(SEL) $4 = <no value available>
(lldb) p $3.buckets()[1].sel()
(SEL) $5 = "class"
(lldb) p $3.buckets()[2].sel()
(SEL) $6 = <no value available>
(lldb) p $3.buckets()[3].sel()
(SEL) $7 = <no value available>
(lldb) p $3.buckets()[4].sel()
(SEL) $8 = "sayHello"
(lldb) p $3.buckets()[4].imp(nil,[NBPerson class])
(IMP) $9 = 0x0000000100003e00 (KCObjcBuild`-[NBPerson sayHello] at main.m:33)
(lldb) 

  • 调用sayHello后,_mayMask和occupied被赋值,这两个变量应该和缓存是有关系
  • bucket_t结构提供了sel()和imp(nil,[NBPerson class])方法
  • sayhello方法的sel和imp,存在bucket中而buckets存在cache中

这种方式需要objc源码,而且断点调试有点不够暴力

  • 脱离源码咱们也能测试


//这里我们模仿objc源码
typedef uint32_t mask_t;
struct nb_bucket_t {
    SEL _sel;
    IMP _imp;
};
struct nb_cache_t{
      struct nb_bucket_t * _buckets;
      mask_t      _maybeMask;
      uint16_t    _flags;
      uint16_t    _occupied;
};
 

struct nb_class_data_bits_t{
     uintptr_t bits;
};

 
struct nb_objc_class  {
    Class ISA;
    Class superclass;
    struct nb_cache_t cache;
    struct nb_class_data_bits_t bits;
};

int main(int argc, const char * argv[]) {
   
    
    @autoreleasepool {
        
        NBPerson *nb = [NBPerson alloc];
        
        [nb sayHello];
        
        Class nbClass  = [NBPerson class];
            struct nb_objc_class * nb_class = (__bridge struct nb_objc_class *)(nbClass);
            NSLog(@" - %hu  - %u",nb_class->cache._occupied,nb_class->cache._maybeMask);
            
            for (int i = 0; i < nb_class->cache._maybeMask; i++) {
                struct nb_bucket_t bucket =nb_class->cache._buckets[i];
                NSLog(@"%@ - %p",NSStringFromSelector(bucket._sel),bucket._imp);
              }

    }
    return NSApplicationMain(argc, argv);
}
  • 运行代码输出结果
//注释 [nb sayHello];输出结果
2021-08-06 17:10:51.921202+0800 对象与isa之间的关系[54912:852133]  - 0  - 0

//放开 [nb sayHello];输出结果
2021-08-06 17:14:17.837195+0800 对象与isa之间的关系[55083:855095] hello----
2021-08-06 17:14:17.837264+0800 对象与isa之间的关系[55083:855095]  - 1  - 3
2021-08-06 17:14:17.837295+0800 对象与isa之间的关系[55083:855095] (null) - 0x0
2021-08-06 17:14:17.837348+0800 对象与isa之间的关系[55083:855095] sayHello - 0x63e0
2021-08-06 17:14:17.837370+0800 对象与isa之间的关系[55083:855095] (null) - 0x0

  • 然后我们加上
        nb.name = @"name";
        

//输出结果
2021-08-06 17:24:54.976660+0800 对象与isa之间的关系[55612:865082] hello----
2021-08-06 17:24:54.976735+0800 对象与isa之间的关系[55612:865082]  - 2  - 3
2021-08-06 17:24:54.976793+0800 对象与isa之间的关系[55612:865082] sayHello - 0x1a2e0
2021-08-06 17:24:54.976835+0800 对象与isa之间的关系[55612:865082] setName: - 0x1a360
2021-08-06 17:24:54.976856+0800 对象与isa之间的关系[55612:865082] (null) - 0x0
  • 再加上
        [nb sayHello1];
        [nb sayHello2];
        [nb sayHello3];
        [nb sayHello4];
        [nb sayHello5];
        
//输出结果

2021-08-06 17:26:16.365782+0800 对象与isa之间的关系[55671:866374] hello----
2021-08-06 17:26:16.365848+0800 对象与isa之间的关系[55671:866374] hello----1
2021-08-06 17:26:16.365875+0800 对象与isa之间的关系[55671:866374] hello----2
2021-08-06 17:26:16.365894+0800 对象与isa之间的关系[55671:866374] hello----3
2021-08-06 17:26:16.365911+0800 对象与isa之间的关系[55671:866374] hello----4
2021-08-06 17:26:16.365926+0800 对象与isa之间的关系[55671:866374] hello----5
2021-08-06 17:26:16.365943+0800 对象与isa之间的关系[55671:866374]  - 5  - 7
2021-08-06 17:26:16.365989+0800 对象与isa之间的关系[55671:866374] sayHello2 - 0x1a308
2021-08-06 17:26:16.366011+0800 对象与isa之间的关系[55671:866374] sayHello3 - 0x1a338
2021-08-06 17:26:16.366029+0800 对象与isa之间的关系[55671:866374] (null) - 0x0
2021-08-06 17:26:16.366062+0800 对象与isa之间的关系[55671:866374] sayHello4 - 0x1a368
2021-08-06 17:26:16.366172+0800 对象与isa之间的关系[55671:866374] (null) - 0x0
2021-08-06 17:26:16.366274+0800 对象与isa之间的关系[55671:866374] sayHello1 - 0x1a0d8
2021-08-06 17:26:16.366338+0800 对象与isa之间的关系[55671:866374] sayHello5 - 0x1a398

  • 通过调试,我们知道了调用方法会有缓存 从结果中我们会发现一些奇怪的地方
  1. 输出的顺序不对
  2. 输出的方法有一些不在里面

继续源码分析

//insert为插入事件⌚️
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;
/// 1. 这里先判断是否是第一次执行,如果是就去开辟内存。
        reallocate(oldCapacity, capacity, /* freeOld */false);//这里false代表新开辟内存
    }
    else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
/// 2. 这里表示当前的buckets 的mask 数量大于 occupied,就不用开辟新的内存空间
    }
#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 {
/// 3. 这里表示要插入的的bucket 没有了空间,需要扩容并且重新开辟新的内存,并且释放掉之前旧的内存空间。
/// 比如,当前有2个缓存,然后当要插入第 3 个缓存的时候,newOccupied = 3,newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity) = 3 不成了,所以就会扩容。
/// 至于为什么要释放旧的内存空间,大概是为了优化内存,长时间没用的缓存就释放掉,等下一次调用的时候在重新缓存。
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;  ///这里也限制了,最多可以有 2^16 个缓存
        }
        reallocate(oldCapacity, capacity, true);//这里true代表已有内存需要扩容
    }

    bucket_t *b = buckets();
    mask_t m = capacity - 1;  
    mask_t begin = cache_hash(sel, m);  /// 采用哈希算法获取当前的bucket 需要存放的位置,
    mask_t i = begin;

    /// 4. 这里就是插入 bucket 到指定的缓存位置。
    do {
        if (fastpath(b[i].sel() == 0)) {
/// 如果当前的位置不存在数据,或者为一片空的内存空间,那么就插入这个bucket。并且让 _occupied 自增1。
            incrementOccupied();
            b[i].set<Atomic, Encoded>(b, sel, imp, cls());
            return;
        }
        if (b[i].sel() == sel) {
             /// 如果当前 bucket 里面的sel 和 将要存入的一样,则跳过。
            return;
        }

/// 这里是循环的终止条件,一直寻找下一个 i 直到找到了最初的位置,即处理hash 冲突
    } while (fastpath((i = cache_next(i, m)) != begin));

    bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}
  • 然后开辟内存的方法
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);
    }
}

.....

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);
    // 这里开辟了一段连续的空间,用于存放 ‘newCapacity’ 个 ‘bucket_t’ 结构体。
......

void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{ 
    // ensure other threads see new buckets before new mask
    _maybeMask.store(newMask, memory_order_release);
/// 通过这个方法,就重新设置里buckets * 和 maybeMask 这两个数据了
......

///通过这个方法,我们知道开辟的内存为大小为 newCapacity * sizeof(bucket) 的大小(即 16 的倍数),所以获取指定的 bucket_t 都可以通过内存平移的方式去寻找,因为这里开辟的内存是连续的,并且存放 bucket_t  数据。

代码的大致逻辑是

  1. 当前有没有缓存 buckets 如果没有,那么就调用 reallocate -> allocateBuckets 去开辟新的缓存内存空间;
  2. 如果有缓存空间并且空间的3/4足够插入当前要缓存的 bucket那么就直接通过 hash 算法寻找对应的缓存下标去缓存对应的数据;
  3. 如果缓存空间的3/4不足一插入下一个 bucket,那么就开辟新的扩容空间并且释放掉之前的空间,把当前的数据插入到指新的空间里面。

然后分析一波运行结果

  • _mask 是当前缓存可以存放的缓存个数的大小,其值为常量 capacity - 1

  • _occupied 是当前缓存的具体个数

  • 第一次运行结果 _occupid和_mask 1,3 //1个缓存,缓存空间3

  • 第二次运行结果 _occupid和_mask 2,3 //2个缓存,缓存空间3

  • 第三次运行结果 _occupid和_mask 5,7 //5个缓存,缓存空间7

  • 前两次结果容易理解,第三次为什么让人不解呢? 第三次是调用了7个方法,当调用到第三个方法的时候(3+1)>(3+1)*(3/4)(源码逻辑第三条)

这时会做两件事
  1. 扩容:就要扩容(3+1)*2-1 = 7
  2. 清除:清除之前的两个缓存

清除缓存源码

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;
    cache_t::collectNolock(false);
}

  • bucket下标计算

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

#if CACHE_END_MARKER
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;
}

cache_hash主要是生成hash下标,cache_next主要是解决hash冲突

二,imp编码解码

bucket中的的imp地址,存储的是经过编码以后强转成uintptr_t类型数据,解码是会还原成原来的imp

  • 编码源码

#if __arm64__

template<Atomicity atomicity, IMPEncoding impEncoding>
void bucket_t::set(bucket_t *base, SEL newSel, IMP newImp, Class cls)
{
    ASSERT(_sel.load(memory_order_relaxed) == 0 ||
           _sel.load(memory_order_relaxed) == newSel);

    static_assert(offsetof(bucket_t,_imp) == 0 &&
                  offsetof(bucket_t,_sel) == sizeof(void *),
                  "bucket_t layout doesn't match arm64 bucket_t::set()");

    uintptr_t encodedImp = (impEncoding == Encoded
                            ? encodeImp(base, newImp, newSel, cls)
                            : (uintptr_t)newImp);

    // LDP/STP guarantees that all observers get
    // either imp/sel or newImp/newSel
    stp(encodedImp, (uintptr_t)newSel, this);
}

#else

template<Atomicity atomicity, IMPEncoding impEncoding>
void bucket_t::set(bucket_t *base, SEL newSel, IMP newImp, Class cls)
{
    ASSERT(_sel.load(memory_order_relaxed) == 0 ||
           _sel.load(memory_order_relaxed) == newSel);

    // objc_msgSend uses sel and imp with no locks.
    // It is safe for objc_msgSend to see new imp but NULL sel
    // (It will get a cache miss but not dispatch to the wrong place.)
    // It is unsafe for objc_msgSend to see old imp and new sel.
    // Therefore we write new imp, wait a lot, then write new sel.
    
    //原有的imp进行编码(和class进行异或运算)转成 uintptr_t 类型数据
    uintptr_t newIMP = (impEncoding == Encoded
                        ? encodeImp(base, newImp, newSel, cls)
                        : (uintptr_t)newImp);

    if (atomicity == Atomic) {
        _imp.store(newIMP, memory_order_relaxed);
        
        if (_sel.load(memory_order_relaxed) != newSel) {
#ifdef __arm__
            mega_barrier();
            _sel.store(newSel, memory_order_relaxed);
#elif __x86_64__ || __i386__
            _sel.store(newSel, memory_order_release);
#else
#error Don't know how to do bucket_t::set on this architecture.
#endif
        }
    } else {
        _imp.store(newIMP, memory_order_relaxed);
        _sel.store(newSel, memory_order_relaxed);
    }
}

#endif

  • 解码

    inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
        uintptr_t imp = _imp.load(memory_order_relaxed);
        if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
        SEL sel = _sel.load(memory_order_relaxed);
        return (IMP)
            ptrauth_auth_and_resign((const void *)imp,
                                    ptrauth_key_process_dependent_code,
                                    modifierForSEL(base, sel, cls),
                                    ptrauth_key_function_pointer, 0);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
        return (IMP)(imp ^ (uintptr_t)cls);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
        return (IMP)imp;
#else
#error Unknown method cache IMP encoding.
#endif
    }

异或运算解析

  • 异或运算:参与运算的两个值,如果两个相应位相同,则结果为0,否则为1
  • 异是同否
int a = 3; int b = 12; int c = 15;
        NSLog(@"%d-%d-%d",a,b,c);


//调试
(lldb) p a ^ b
(int) $0 = 15
(lldb) p c^ b
(int) $1 = 3
(lldb) 
        

二进制解析

a = 3  0000 0011 
b = 12 0000 1100 
a ^ b 0000 0011 ^ 0000 1100 = 0000 1111 = 15 
c ^ b 0000 1111 ^ 0000 1100 = 0000 0011 = 3