类的结构分析-cache_t

283 阅读7分钟
struct objc_class : objc_object {
  objc_class(const objc_class&) = delete;
  objc_class(objc_class&&) = delete;
  void operator=(const objc_class&) = delete;
  void operator=(objc_class&&) = delete;
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits; 
  }

上一部分分析了class中的bits,本篇文章探究类结构中的cache_t,主要从两种方法来探究,lldb,脱离源码结构,这两种方法来探究。

lldb探究

探究的环境如下:

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic) int age;
@property (nonatomic, strong) NSString *hobby;

- (void)sayHappy;


@end

@implementation Person

- (void)sayHappy{
    NSLog(@"%s",__func__);
}


@end

既然是缓存,那么调用之后才会产生,先调用一下

   Person *p  = [Person alloc];
   [p sayHappy];

打印cache如下,p (cache_t *)0x100008428(由于类的结构,偏移16得到)

截屏2021-06-23 上午11.52.54.png 看到里面有_occupied = 1。 看一下cache_t的结构

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask; // 4
#if __LP64__
            uint16_t                   _flags;  // 2
#endif
            uint16_t                   _occupied; // 2
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 8
    };
    ......
    public:
    // The following four fields are public for objcdt's use only.
    // objcdt reaches into fields while the process is suspended
    // hence doesn't care for locks and pesky little details like this
    // and can safely use these.
    unsigned capacity() const;
    struct bucket_t *buckets() const;
   ......
    void insert(SEL sel, IMP imp, id receiver);
 
    }

从结构中看到,里面有一个buckets()我们来取出buckets(); 打印如下 截屏2021-06-23 下午12.01.25.png 我们看一下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
public:
    static inline size_t offsetOfSel() { return offsetof(bucket_t, _sel); }
    inline SEL sel() const { return _sel.load(memory_order_relaxed); }
    ....
 inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
 .....
 }
}

取出里面的sel和imp, 截屏2021-06-23 下午12.05.08.png ❓添加一个加号方法,也会在里面吗,我们来测试一下,

Person.h
@interface Person : NSObject
...
+ (void)saySad;
@end

Person.m
@implementation Person

...
+ (void)saySad {
    NSLog(@"%s",__func__);
}
...

@end
main.m
...
[Person saySad];

打印一下Person里面的cache_t 截屏2021-06-23 下午12.14.25.png _oocupied仍然是1,说明不在Person的cache_t中,那在哪里呢,我们测试一下元类

截屏2021-06-24 下午3.21.27.png

截屏2021-06-24 下午3.22.42.png 可以看到类方法在元类的cache_t里面

脱离源码

我们可以在源码中看到cache_t的数据结构,我们如果把部分数据结构复制出来,然后进行打印,是不是更加直观一些? 我们可以把数据结构复制出来一些 研究对象

@interface Person : NSObject

- (void)sayHappy1;
- (void)sayHappy2;
- (void)sayHappy3;
- (void)sayHappy4;
- (void)sayHappy5;

+ (void)sayno;

@end

@implementation Person

- (void)sayHappy1 {
    NSLog(@"__%s__", __func__);
    
}
- (void)sayHappy2 {
    NSLog(@"__%s__", __func__);
    
}
- (void)sayHappy3 {
    NSLog(@"__%s__", __func__);
    
}
- (void)sayHappy4 {
    NSLog(@"__%s__", __func__);
    
}
- (void)sayHappy5 {
    NSLog(@"__%s__", __func__);
    
}

+ (void)sayno {
    
}

@end
typedef uint32_t mask_t;

struct dw_bucket_t {
    SEL _sel;
    IMP _imp;
};

struct dw_cache_t {
    
    struct dw_bucket_t *_buckets;
    mask_t      _maybeMask;
    uint16_t    _flags;
    uint16_t    _occupied;
   
};
struct dw_class_data_bits_t {
    uintptr_t bits;
};
struct dw_objc_class  {
    Class isa;
    Class superclass;
    struct dw_cache_t cache;             // formerly cache pointer and vtable
    struct dw_class_data_bits_t bits;
};

     Person *p = [Person alloc];
     [p sayHappy1];
     [p sayHappy2];
     [Person sayno];
     
     struct dw_objc_class* pclass = (__bridge struct dw_objc_class *)(p.class);
     NSLog(@"%u_%u", pclass->cache._occupied, pclass->cache._maybeMask);
    for (int i = 0; i < pclass->cache._maybeMask; i++) {
            struct dw_bucket_t bucket = pclass->cache._buckets[i];
            NSLog(@"%@ - %pf",NSStringFromSelector(bucket._sel),bucket._imp);
     }        

打印结果如下:

截屏2021-06-23 下午1.37.26.png 由于分配了3个空间,但是有两个方法,打印的时候有时候是空的。 我们调用3次,结果如下

  Person *p = [Person alloc];
  [p sayHappy1];
  [p sayHappy2];
  [p sayHappy3];
  
 /**
2021-06-23 13:45:53.212623+0800 cache_t[5544:349832] 1_7
2021-06-23 13:45:53.212775+0800 cache_t[5544:349832] sayHappy3 - 0x8ad8f
2021-06-23 13:45:53.212852+0800 cache_t[5544:349832] (null) - 0x0f
2021-06-23 13:45:53.212937+0800 cache_t[5544:349832] (null) - 0x0f
2021-06-23 13:45:53.213007+0800 cache_t[5544:349832] (null) - 0x0f
2021-06-23 13:45:53.213077+0800 cache_t[5544:349832] (null) - 0x0f
2021-06-23 13:45:53.213147+0800 cache_t[5544:349832] (null) - 0x0f
2021-06-23 13:45:53.213299+0800 cache_t[5544:349832] (null) - 0x0f
**/

❓为什么变成了1_7 我们在多调用几次


  Person *p = [Person alloc];
  [p sayHappy1];
  [p sayHappy2];
  [p sayHappy3];
  [p sayHappy4];
  [p sayHappy2];
  
  /**
2021-06-23 13:48:24.101867+0800 cache_t[5579:352651] 3_7
2021-06-23 13:48:24.102031+0800 cache_t[5579:352651] sayHappy3 - 0x18b20f
2021-06-23 13:48:24.102108+0800 cache_t[5579:352651] (null) - 0x0f
2021-06-23 13:48:24.102197+0800 cache_t[5579:352651] sayHappy4 - 0x18af0f
2021-06-23 13:48:24.102277+0800 cache_t[5579:352651] (null) - 0x0f
2021-06-23 13:48:24.102351+0800 cache_t[5579:352651] (null) - 0x0f
2021-06-23 13:48:24.102445+0800 cache_t[5579:352651] (null) - 0x0f
2021-06-23 13:48:24.102624+0800 cache_t[5579:352651] sayHappy2 - 0x18b10f
(lldb) 
  **/

我们发现当有3个的时候发生了改变,之前的清空了,容量变大了,新存储了新的值 我们看一下源码。在之前上面的cache_t的源码中有一个insert函数,我们来看一下内部实现。

void cache_t::insert(SEL sel, IMP imp, id receiver)
{
....
mask_t newOccupied = occupied() + 1; 
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    if (slowpath(isConstantEmptyCache())) { //第一次进来走这里
      
        if (!capacity) capacity = INIT_CACHE_SIZE; 
        //   INIT_CACHE_SIZE_LOG2 = 2,
        //   INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2),
        //   1<<2  所以capacity = 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);
    }
    
    /**以下是一个hash存值的过程*/
    bucket_t *b = buckets();
    mask_t m = capacity - 1;
    mask_t begin = cache_hash(sel, m);
    mask_t i = begin;
      do {
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();// _occupied++;
            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);

}

由注释只,第一次进到if (slowpath(isConstantEmptyCache())) { 这个分支里面,我们看下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);
    //设置buckets和mask
    
    if (freeOld) {
        collect_free(oldBuckets, oldCapacity);
    }
}

我们看一下setBucketsAndMask函数

void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
    // objc_msgSend uses mask and buckets with no locks.
    // It is safe for objc_msgSend to see new buckets but old mask.
    // (It will get a cache miss but not overrun the buckets' bounds).
    // It is unsafe for objc_msgSend to see old buckets and new mask.
    // Therefore we write new buckets, wait a lot, then write new mask.
    // objc_msgSend reads mask first, then buckets.

#ifdef __arm__
    // ensure other threads see buckets contents before buckets pointer
    mega_barrier();

    _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;
#elif __x86_64__ || i386
    // ensure other threads see buckets contents before buckets pointer
    _bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_release);

    // ensure other threads see new buckets before new mask
    _maybeMask.store(newMask, memory_order_release);//第一次进来newMask为3
    _occupied = 0;//还没有赋值
#else
#error Don't know how to do setBucketsAndMask on this architecture.
#endif
}

从上面的代码我们知道第一次进来缓存时: _maybeMask.store(newMask, memory_order_release); _maybeMask = 3, 当后面的在缓存的时候 if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity)))

mask_t newOccupied = occupied() + 1;
#define CACHE_END_MARKER 1
static inline mask_t cache_fill_ratio(mask_t capacity) {
    return capacity * 3 / 4;
}

所以上面的条件就变为当前缓存方法的个数+1+1<=3/4分配的空间,即缓存小于容量的3/4时,大于3/4时走else

{      //进行2倍扩容 当为4个方法时capacity = 8
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        reallocate(oldCapacity, capacity, true);
    }

所以当有4个方法时capacity = 8, maybeMask = 7; 在reallocate中,第二次进来的时候freeOld为true,便进行一个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;
    cache_t::collectNolock(false);
}

抹掉老的内存。所以为3的时候,进行了扩容,抹掉了之前的内存,就只有一个函数了。

缓存模拟器模拟器流程图如下:

截屏2021-06-23 下午3.50.51.png

补充

调用入口

insert里面添加一个断点,可以得到上面的流程图,但是什么时候触发的lookUpImpOrForward,我们搜到cache_t的时候卡在objc-cache.mm文档中可以看到,有如下一段话

 Cache readers (PC-checked by collecting_in_critical())
 * objc_msgSend*
 * cache_getImp
 *
 * Cache readers/writers (hold cacheUpdateLock during access; not PC-checked)
 * cache_t::copyCacheNolock    (caller must hold the lock)
 * cache_t::eraseNolock        (caller must hold the lock)
 * cache_t::collectNolock      (caller must hold the lock)
 * cache_t::insert             (acquires lock)
 * cache_t::destroy            (acquires lock)
 *
 * UNPROTECTED cache readers (NOT thread-safe; used for debug info only)
 * cache_print
 * _class_printMethodCaches
 * _class_printDuplicateCacheEntries
 * _class_printMethodCacheStatistics

缓存的读取是在objc_msgSend这个流程中进行的吗,接下来我们就来探究objc_msgSend这个流程。

lldb下的扩容情况

表现代码:

@interface Person : NSObject

// isa  8
@property (nonatomic, copy) NSString *name; //8
@property (nonatomic) int age; //4
@property (nonatomic) int score; //4
@property (nonatomic) int score1; //4

- (void)saySomething;
- (void)saySomething1;
- (void)saySomething2;
- (void)saySomething3;
- (void)saySomething4;

@end

@implementation Person
- (void)saySomething{
    NSLog(@"%s",__func__);
}
- (void)saySomething1 {
    NSLog(@"%s",__func__);
    
}
- (void)saySomething2 {
    NSLog(@"%s",__func__);
    
}
- (void)saySomething3 {
    NSLog(@"%s",__func__);
    
}
- (void)saySomething4 {
    NSLog(@"%s",__func__);
    
}

@end


截屏2021-06-28 下午3.13.58.png 我们用lldb来调用[p saySomething] 然后再看*$1,我们发现如下, _maybeMask = { std::__1::atomic<unsigned int> = { Value = 7 } ❓这是为什么呢,是在什么时候扩容了吗

截屏2021-06-28 下午3.14.20.png 先在insert中插入一行打印,看都有哪些方法

void cache_t::insert(SEL sel, IMP imp, id receiver)
{
    runtimeLock.assertLocked();
    
    printf("\n走insert%p--%p--%p\n", sel, imp, receiver);
    ...
  }
  
  /*
走insert0x7fff7b869cce--0x10034ff20--0x1006a3460

走insert0x7fff7b869c33--0x10034fb80--0x1006a3460

走insert0x100367b21--0x100003b10--0x1006a3460
  */
  

打印p<Person: 0x1006a3460>,我们看到上面0x1006a3460中有三个,分别打印下SEL

截屏2021-06-28 下午3.47.12.png 在此之前已经有了两个方法class和respondsToSelector, 我们把打印范围缩小,打印下p [p saySomething]之前的所有方法

    mask_t newOccupied = occupied() + 1;
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    
    if (sel ==@selector(saySomething)) {
        bucket_t *dw_b = buckets();
        for (unsigned i = 0; i <  oldCapacity; i++) {
            SEL dw_sel = dw_b[i].sel();
            IMP dw_imp = dw_b[i].imp(dw_b, nil);
            printf("%p--%p--%p\n", dw_sel, dw_imp, &dw_b[i]);
        }
        printf("isConstantEmptyCache %p -%u-%u-%u---", dw_b, capacity, newOccupied, oldCapacity);
    }
    
    if (slowpath(isConstantEmptyCache())) {

截屏2021-06-28 下午3.52.41.png 看到里面有三个方法,第一个和第三个分别是上面的class和respondsToSelector,但是ox1这个是是什么呢,我们看一下源码: 点开allocateBuckets的代码

bucket_t *cache_t::allocateBuckets(mask_t newCapacity)
{
    ....
    // End marker's sel is 1 and imp points to the first bucket.
    end->set<NotAtomic, Raw>(newBuckets, (SEL)(uintptr_t)1, (IMP)newBuckets, nil);
    ...
    return newBuckets;
}

看到在最后会插入一个1,imp指向第一个bucket的首地址; 所以最后一个是0x1--0x10142b360--0x10142b390,当调用[p saySomething]的时候会进行一个扩容。

但是objc的源码环境下 调用第二个方法时:此时缓存一个方法

截屏2021-06-28 下午4.02.02.png

截屏2021-06-28 下午4.02.11.png 调用第四个方法时,扩容,只缓存了第三个方法,是在调用第三个方法的时候进行扩容的。 截屏2021-06-28 下午4.03.42.png 猜测:可能是编译器对于打印做了优化,过滤掉了,系统自动调用的函数。