5、iOS底层分析 - cache_t 分析

分析cache_t 之前先看一下 objc_class,类在底层的结构(objc-runtime-new.h中)。

我们知道了isa是指向元类的,superclass 是父类相关,bits里面存的是类的属性和实例变量以及实例方法。剩下的cache 看名字是缓存,现在来看一下具体的

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

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

可以看到 cache 是一个cache_t结构体,在objc源码的objc-runtime-new.h中可以看到定义

struct cache_t {
    struct bucket_t *_buckets; // 8
    mask_t _mask;  // uint32_t 4
    mask_t _occupied; // 4
public:
    struct bucket_t *buckets();
    mask_t mask();
    mask_t occupied();
    void incrementOccupied();
    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
    void initializeToEmpty();

    mask_t capacity();
    bool isConstantEmptyCache();
    bool canBeFreed();

    static size_t bytesForCapacity(uint32_t cap);
    static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);

    void expand();
    void reallocate(mask_t oldCapacity, mask_t newCapacity);
    struct bucket_t * find(cache_key_t key, id receiver);

    static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn));
};
typedef uint32_t mask_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__
    MethodCacheIMP _imp;
    cache_key_t _key;
#else
    cache_key_t _key;
    MethodCacheIMP _imp;
#endif
public:
    inline cache_key_t key() const { return _key; }
    inline IMP imp() const { return (IMP)_imp; }
    inline void setKey(cache_key_t newKey) { _key = newKey; }
    inline void setImp(IMP newImp) { _imp = newImp; }

    void set(cache_key_t newKey, IMP newImp);
};
  • cache_t 的内部定义了三个成员,分别为mask_t类型的 _mask和_occupied,以及一个bucket_t的结构体指针
  • 其中 mask_t 可以看出是一个无符号 Int 类型,在64位下为 uint32_t
  •   bucket_t  则是存放着 imp key

1、cache_t

开始通过源码可以知道猜测这个cache_t 是用来缓存方法的。下面进行验证

断点打在 sayHello 调用之后。按前面的猜想,LGPerson 的alllocclass  、sayHello 方法都调用了都有缓存。

同时呢上一篇分析类的结构的时候,我们知道类的  实例(对象)方法 在类的 bits 里面存储。类方法 在元类中存储,按这个思路的话那么缓存应该也是对应的,进行验证。

实例(对象)方法 缓存

(lldb) x/4gx pClass
0x1000012e0: 0x001d8001000012b9 0x0000000100b36140
0x1000012f0: 0x0000000101e23c20 0x0000000100000003
(lldb) p (cache_t *)0x1000012f0
(cache_t *) $1 = 0x00000001000012f0
(lldb) p *$1
(cache_t) $2 = {
  _buckets = 0x0000000101e23c20
  _mask = 3
  _occupied = 1
}
(lldb) p $2._buckets
(bucket_t *) $3 = 0x0000000101e23c20
(lldb) p *$3
(bucket_t) $4 = {
  _key = 4294971020
  _imp = 0x0000000100000c60 (LGTest`-[LGPerson sayHello] at LGPerson.m:13)
}
(lldb) 

 对象方法  缓存

(lldb) x/4gx person
0x1018037d0: 0x001d8001000012ed 0x0000000000000000
0x1018037e0: 0x0000000000000000 0x0000000000000000
(lldb) p/x 0x001d8001000012ed & 0x00007ffffffffff8ULL
(unsigned long long) $1 = 0x00000001000012e8
(lldb) x/4gx $1
0x1000012e8: 0x001d8001000012c1 0x0000000100afe140
0x1000012f8: 0x000000010183e4c0 0x0000000200000003
(lldb) p (cache_t *)0x1000012f8
(cache_t *) $2 = 0x00000001000012f8
(lldb) p * $2
(cache_t) $3 = {
  _buckets = 0x000000010183e4c0
  _mask = 3
  _occupied = 2
}
(lldb) p $3._buckets
(bucket_t *) $4 = 0x000000010183e4c0
(lldb) p *$4
(bucket_t) $5 = {
  _key = 4298994200
  _imp = 0x00000001003cc3b0 (libobjc.A.dylib`::+[NSObject alloc]() at NSObject.mm:2294)
}
(lldb) 

通过LLDB调试,可以发现确实和 实例方法和类方法的存储是一只的,缓存也是,实例方法缓存在类中,类方法缓存在元类中

由上的打印结果 sayHello  和 alloc 可知

2、cache的策略

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [[LGPerson alloc] init];//1
        Class pClass = [LGPerson class];
        // cache_t 为什么没有 - 第一次
        [person sayHello];//2
        [person sayCode];//3
        [person sayNB]; //4

        // 临界点 - 清理 过去 - 扩容 - cache_t 缓存
        // cache_t mask
        // 来一个方法就缓存 - 有一个特殊的处理

        struct lg_objc_class *lg_pClass = (__bridge struct lg_objc_class *)(pClass);
        for (mask_t i = 0; i<lg_pClass->cache._mask; i++) {
            struct lg_bucket_t bucket = lg_pClass->cache._buckets[i];
            NSLog(@"%lu - %p",bucket._key,bucket._imp);
        }
        NSLog(@"%@ - %p",person,pClass);

    }
    return 0;
}

 

  1. 将断点打在 [person sayCode]; 
    1. 打印出来的结果  mask  是3
  2. 将断点打在 [person sayNB]; 之后
    1. 打印出来的结果 mask 是7

那么问题来了:

我们一共调用了 init,sayHello,sayCode,sayNB 一共4个实例方法。mask 显示 3 后是 7. 打印一下 buckets 看看结果,发现只有 _buckets[2] 里面存了一个我们最后调用的 sayNB,那么其他方法都去哪了?

2019-12-25 00:57:52.143504+0800 LGTest[3662:48762] LGPerson say : -[LGPerson sayHello]
2019-12-25 00:57:52.144031+0800 LGTest[3662:48762] LGPerson say : -[LGPerson sayCode]
2019-12-25 00:57:52.144133+0800 LGTest[3662:48762] LGPerson say : -[LGPerson sayNB]
(lldb) x/4gx pClass
0x1000012e8: 0x001d8001000012c1 0x0000000100b36140
0x1000012f8: 0x0000000101029950 0x0000000100000007
(lldb) p (cache_t *)0x1000012f8
(cache_t *) $1 = 0x00000001000012f8
(lldb) p *$1
(cache_t) $2 = {
  _buckets = 0x0000000101029950
  _mask = 7
  _occupied = 1
}
(lldb) p $2._buckets
(bucket_t *) $3 = 0x0000000101029950
(lldb) p *$3
(bucket_t) $4 = {
  _key = 0
  _imp = 0x0000000000000000
}
(lldb) p $2._buckets[0]
(bucket_t) $5 = {
  _key = 0
  _imp = 0x0000000000000000
}
(lldb) p $2._buckets[1]
(bucket_t) $6 = {
  _key = 0
  _imp = 0x0000000000000000
}
(lldb) p $2._buckets[2]
(bucket_t) $7 = {
  _key = 4294971026
  _imp = 0x0000000100000ce0 (LGTest`-[LGPerson sayNB] at LGPerson.m:25)
}
(lldb) p $2._buckets[3]
(bucket_t) $8 = {
  _key = 0
  _imp = 0x0000000000000000
}
(lldb) p $2._buckets[5]
(bucket_t) $9 = {
  _key = 0
  _imp = 0x0000000000000000
}
(lldb) p $2._buckets[6]
(bucket_t) $10 = {
  _key = 0
  _imp = 0x0000000000000000
}
(lldb) p $2._buckets[7]
(bucket_t) $11 = {
  _key = 0
  _imp = 0x0000000000000000
}

分析源码

1、cache_t

源码最上边

2、ask_t mask()

mask_t cache_t::mask() 
{
    return _mask; 
}

3、mask_t capacity();

继续搜索mask(),发现在capacity方法中有mask的相应操作

mask_t cache_t::capacity() 
{
    return mask() ? mask()+1 : 0; 
}

4、cache_t::expand()

全局搜索(objc-cache.mm capacity() ,看一下在哪个地方调用了这个方法。找到了方法 cache_t::expand() 。方法内部看到了capacity 的调用,意思是,如果 oldCapacity 获取的值为0,那么久用 INIT_CACHE_SIZE(1<<2  1左移两位十六进制得到的 实际为4)来初始化,如果存在,那么就用 oldCapacity的2倍 来作为 newCapacity

/* Initial cache bucket count. INIT_CACHE_SIZE must be a power of two. */
enum {
    INIT_CACHE_SIZE_LOG2 = 2,
    INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2)
};
void cache_t::expand()
{
    cacheUpdateLock.assertLocked();
    
    uint32_t oldCapacity = capacity();
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;

    if ((uint32_t)(mask_t)newCapacity != newCapacity) {
        // mask overflow - can't grow further
        // fixme this wastes one bit of mask
        newCapacity = oldCapacity;
    }

    reallocate(oldCapacity, newCapacity);
}

cache_fill_nolock

虽然我看看到了在 expand() 中进行了扩容,但是什么条件下才会去执行扩容还是不知道,这个时候就全局(objc-cache.mm)的去搜索 expand() 找到了方法  cache_fill_nolock

static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
    cacheUpdateLock.assertLocked();

    // Never cache before +initialize is done
    if (!cls->isInitialized()) return;

    // Make sure the entry wasn't added to the cache by some other thread 
    // before we grabbed the cacheUpdateLock.
    if (cache_getImp(cls, sel)) return;

    cache_t *cache = getCache(cls);
    cache_key_t key = getKey(sel);

    // Use the cache as-is if it is less than 3/4 full
    mask_t newOccupied = cache->occupied() + 1;
    mask_t capacity = cache->capacity();
    if (cache->isConstantEmptyCache()) {
        // Cache is read-only. Replace it.
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {
        // Cache is too full. Expand it.
        cache->expand();
    }

    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot because the 
    // minimum size is 4 and we resized at 3/4 full.
    bucket_t *bucket = cache->find(key, receiver);
    if (bucket->key() == 0) cache->incrementOccupied();
    bucket->set(key, imp);
}

void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
#if !DEBUG_TASK_THREADS
    mutex_locker_t lock(cacheUpdateLock);
    cache_fill_nolock(cls, sel, imp, receiver);
#else
    _collecting_in_critical();
    return;
#endif
}

cache_fill_nolock  源码可以看到,如果没有初始化clas 直接返回,如果找到了对象方法的缓存也直接返回。 

下面才是重点  newOccupied = cache->occupied() + 1; 用临时变量计算出加上当前方法之后新的缓存需要的内存大小 然后进行判断 ,如果当前所需要的内存大小 > 当前cache_t 已经开辟的内存大小的 3/4 ,那就执行 cache->expand(); 进行扩容。

知道了先进行判断,然后进行缓存,那就顺着这两个方法去看源码看看初开辟了多大的内存?然后扩容后的方法缓存为什么只剩下最后调用的 sayNB 的问题。

  1. 这个最初开辟了对大的内存空间,这个在 expand() 中看到了 INIT_CACHE_SIZE(1<<2  1左移两位十六进制得到的 实际为4)  最初开辟的空间大小是4
    1. 既然开辟的最初大小是4,那么最开始打印出来  mask  为什么是3呢?扩容后应该是4*2=8,但是扩容后的mask 实际是7(打印看到的)。分析
    2. 在 expand() 中可以看到
    3. cache_t 里面
      void reallocate(mask_t oldCapacity, mask_t newCapacity);
      void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
      {
          bool freeOld = canBeFreed();
      
          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);
          这个地方是计算 mask 的大小,是用 newCapacity-1.类刚初始化的时候是4-1=3.第一次扩容是4*2-1=7
          -1 是一种算法。这个地方之所以用-1.原因
              1、 用3是因为3比4计算更精准,后边的计算超过 3/4 的时候能更精准的计算并提前扩容保证安全。
             
           
          setBucketsAndMask(newBuckets, newCapacity - 1);
          
          if (freeOld) {
              cache_collect_free(oldBuckets, oldCapacity);
              cache_collect(false);
          }
      }
      

      将计算出出来的  newMask  赋值给  mask

    4. void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
      {
          // ensure other threads see buckets contents before buckets pointer
          mega_barrier();
      
          _buckets = newBuckets;
          
          // ensure other threads see new buckets before new mask
          mega_barrier();
          
          _mask = newMask;
          _occupied = 0;
      }
      

       扩容后把之前缓存器中的值全部删除掉

      static void cache_collect_free(bucket_t *data, mask_t capacity)
      {
          cacheUpdateLock.assertLocked();
      
          if (PrintCaches) recordDeadCache(capacity);
      
          _garbage_make_room ();
          garbage_byte_size += cache_t::bytesForCapacity(capacity);
          garbage_refs[garbage_count++] = data;
      }
  2. 分析源码之后知道 cache_fill_nolock 方法内部,可以发现,expand 之后,才会把最新调用的方法的 imp 和 key 缓存了下来,
    1. 如果不需要扩容直接存储。
    2. 如果需要扩容是重新开辟内存空间,然后把之前缓存器中的值全部删除。 这就是为什么 cache 扩容后我们打印出来的只有最后调用的 sayNB 方法,这里就适用了LRU算法,把最近调用过的方法缓存下来

expand 方法的最后,reallocate(oldCapacity, newCapacity)方法,在reallocate方法中,首先使用newCapacity初始化了一个newBuckets,之后设置了新的 buckets 以及 mask,并且最后释放了旧的 oldBuckets,这里之所以直接用 newBuckets 代替,而不是用追加或者修改 oldBuckets 的方式,主要还是为了安全以及执行效率

cache_fill_nolock 的最后 这个端代码就是 将方法缓存下来的操作。

    bucket_t *bucket = cache->find(key, receiver);
    if (bucket->key() == 0) cache->incrementOccupied();
    bucket->set(key, imp);

总结:

  1. cache_fill_nolock 缓存入口进入
  2. 先看有没有缓存,又返回,没有继续找2
  3. getKey(sel)将方法名变成一个数字,便于查找
  4. 创建一个临时变量 occupied +1 (用于判断缓存是否超出开辟的内存大小)
  5. 开辟内存大小为4
  6. 返回的是4-1   3  mask
  7. capacity 是1
  8. 然后去存 set(key , imp)
  9. 有调用方法的时候,然后把缓存数量进行 +1
  10. 判断存储超过3/4的时候进行扩容
  11. 扩成原来的2倍 4*2 = 8
  12. 8-1 mask (这是个算法返回不让达到100%,也是出于安全考虑)
  13. 清除原来的缓存,存储当前的这个方法(copy不安全、同时为了速度更快所以要消除)

 

cache_fill_nolock 缓存入口进入

判断是否有缓存 return

cache_key_t key=getKey(sel)

读取开辟占用occupied

然后判断是否是空的缓存表 cache->isConstantEmptyCache()

  1. 是 :

cache->reallocate  创建一个new Buckets

_mask = newMask

_occupied = 0;

申请完毕后开始填充 bucket_t *bucket =cache->find(key,receiver); 这个就是上一步的reallocate 的时候创建的

然后给bucket 设值 set(key,imp)

  1. 否:

不是第一次缓存,已经存在缓存表

if(newOccupied<=capacity/4*3)  创建临时变量加上新缓存的方法是是否超过开辟内存的3/4

  1. 不超过

正常添加bucket

  1. 超过

cache->expand() 进行扩容 oldCapacity*2 扩容到原来的两倍

然后进行reallocate(oldCapacity,newCapacity); 清理旧内存,从新开辟创建,然后缓存方法,之前缓存的就被清理掉了。Reallocate同上

然后进行填充

bucket_t *bucket=cache->find(key,receiver);

if(bucket->key()==0)

cache->incrementOccupied();

bucket->set(key,imp);

发布了83 篇原创文章 · 获赞 12 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/shengdaVolleyball/article/details/104043980