OC - 类的cache_t分析

156 阅读3分钟

根据源码和lldb分析cache_t

查看objc_class源码

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_rw_t * plus custom rr/alloc flags
    
    //后面代码省略
}

可以看到cache前面有ISAsuperclass,各占8字节。
添加代码

@interface LGPerson : NSObject
- (void)say1;
- (void)say2;
- (void)say3;
- (void)say4;
- (void)say5;
- (void)say6;
- (void)say7;
+ (void)say11;
@end

@implementation LGPerson

- (void)say1{
    NSLog(@"LGPerson say : %s",__func__);
}
- (void)say2{
    NSLog(@"LGPerson say : %s",__func__);
}
- (void)say3{
    NSLog(@"LGPerson say : %s",__func__);
}
- (void)say4{
    NSLog(@"LGPerson say : %s",__func__);
}
- (void)say5{
    NSLog(@"LGPerson say : %s",__func__);
}
- (void)say6{
    NSLog(@"LGPerson say : %s",__func__);
}
- (void)say7{
    NSLog(@"LGPerson say : %s",__func__);
}
+ (void)say11{
    NSLog(@"LGPerson say : %s",__func__);
}
@end

LGPerson *p = [LGPerson alloc];

[p say1];
[p say2];
[p say3];
[p say4];
[p say5];
[p say6];
[p say7];

[LGPerson say11];

lldb调试结果 image.png 查看cache_t源码,并查找打印方法

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;
    };
    
    mask_t mask() const;
    struct bucket_t *buckets() const;
    mask_t occupied() const;
    void insert(SEL sel, IMP imp, id receiver);
    //中间代码省略
}

根据代码里面的打印方法,打印 image.png 发现buckets()并没有我们想要的值。查看插入函数,发现buckets()哈希结构。继续打印buckets() image.png 发现只能打印出say7。查看insert函数,我们发现

void cache_t::insert(SEL sel, IMP imp, id receiver)
{
    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;
        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;

    //中间代码省略
}

static inline mask_t cache_fill_ratio(mask_t capacity) {
    return capacity * 7 / 8;    //__arm64__ && !__LP64__为capacity * 3 / 4;
}

#if CACHE_END_MARKER || (__arm64__ && !__LP64__)
    // When we have a cache end marker it fills a bucket slot, so having a
    // initial cache size of 2 buckets would not be efficient when one of the
    // slots is always filled with the end marker. So start with a cache size
    // 4 buckets.
    INIT_CACHE_SIZE_LOG2 = 2,
#else
    // Allow an initial bucket size of 2 buckets, since a large number of
    // classes, especially metaclasses, have very few imps, and we support
    // the ability to fill 100% of the cache before resizing.
    INIT_CACHE_SIZE_LOG2 = 1,
#endif
    INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2),
    MAX_CACHE_SIZE_LOG2  = 16,
    MAX_CACHE_SIZE       = (1 << MAX_CACHE_SIZE_LOG2),
    FULL_UTILIZATION_CACHE_SIZE_LOG2 = 3,
    FULL_UTILIZATION_CACHE_SIZE = (1 << FULL_UTILIZATION_CACHE_SIZE_LOG2),
};

发现 capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;初始分配内存为INIT_CACHE_SIZE__arm64__ && !__LP64__下为4,其余为2,当occupied超出分配内存时,会重新分配内存,并将原有内存清空。且mask_t m = capacity - 1;
根据上面条件分析总结猜想(调试机型为M1,初始内存分配为2)
当只调用1个say1 occupied为1,mask为1。
当调用2个函数,occupied为2,mask为1。
当调用3个函数,occupied为1,mask为3。
当调用4个函数,occupied为2,mask为3。
当调用5个函数,occupied为3,mask为3。
当调用6个函数,occupied为4,mask为3。
当调用7个函数,occupied为1,mask为7。

脱离源码分析cache_t

如果能用NSLog的方式打印出结果,会节省很多调试时间,基于这个想法,我们将源码自己写一遍。(过程中尽量略去不需要代码)

typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits

struct kc_bucket_t {
    IMP _imp;
    SEL _sel;
};

template <typename T>
struct explicit_atomic : public std::atomic<T> {
    explicit explicit_atomic(T initial) noexcept : std::atomic<T>(std::move(initial)) {}
    operator T() const = delete;
    
    T load(std::memory_order order) const noexcept {
        return std::atomic<T>::load(order);
    }
    void store(T desired, std::memory_order order) noexcept {
        std::atomic<T>::store(desired, order);
    }
    
    static explicit_atomic<T> *from_pointer(T *ptr) {
        static_assert(sizeof(explicit_atomic<T> *) == sizeof(T *),
                      "Size of atomic must match size of original");
        explicit_atomic<T> *atomic = (explicit_atomic<T> *)ptr;
        ASSERT(atomic->is_lock_free());
        return atomic;
    }
};

struct kc_cache_t {
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8
    mask_t    _maybeMask; // 4
    uint16_t  _flags;  // 2
    uint16_t  _occupied; // 2
    
    //下面是M1机型机构下需要添加的代码,其它机型根据条件添加
    static constexpr uintptr_t maskShift = 48;
    static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
    static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << maskShift) - 1;
    
    struct kc_bucket_t *buckets() const;
    mask_t mask() const;
};

struct kc_bucket_t *kc_cache_t::buckets() const
{
    uintptr_t addr = _bucketsAndMaybeMask.load(std::memory_order_relaxed);
    return (kc_bucket_t *)(addr & bucketsMask);
}

mask_t kc_cache_t::mask() const
{
    uintptr_t maskAndBuckets = _bucketsAndMaybeMask.load(std::memory_order_relaxed);
    return maskAndBuckets >> maskShift;
}

struct kc_class_data_bits_t {
    uintptr_t bits;
};

struct kc_objc_class {
    Class isa;
    Class superclass;
    struct kc_cache_t cache;             // formerly cache pointer and vtable
    struct kc_class_data_bits_t bits;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *p = [LGPerson alloc];
        Class pClass = [LGPerson class];
        [p say1];
        [p say2];
        [p say3];
        [p say4];
        [p say5];
        [p say6];
        [p say7];

        struct kc_objc_class *kc_class = (__bridge struct kc_objc_class *)(pClass);
        NSLog(@"%hu - %u",kc_class->cache._occupied,kc_class->cache.mask());
        
        NSInteger i = kc_class->cache._occupied;
        NSInteger count = kc_class->cache.mask();
        for (i = 0; i <= count; i ++) {
            SEL sel = kc_class->cache.buckets()[i]._sel;
            NSString *str = NSStringFromSelector(sel);
            NSLog(@"i = %li buckets = %@   %p",(long)i,str,kc_class->cache.buckets()[i]._imp);
        }
    }
    return 0;
}

打印结果 image.png

关于类方法的猜想以及验证

前面我们知道了,类方法是存在元类里面的,在类里面我们没有找到类方法,那么类方法是否存在元类里。修改代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *p = [LGPerson alloc];
        Class pClass = p.class;
        const char *className = class_getName(pClass);
        Class metaClass = objc_getMetaClass(className);

        [LGPerson say11];

        struct kc_objc_class *kc_class = (__bridge struct kc_objc_class *)(metaClass);
        NSLog(@"%hu - %u",kc_class->cache._occupied,kc_class->cache.mask());
        
        NSInteger i = kc_class->cache._occupied;
        NSInteger count = kc_class->cache.mask();
        for (i = 0; i <= count; i ++) {
            SEL sel = kc_class->cache.buckets()[i]._sel;
           
            NSString *str = NSStringFromSelector(sel);
            NSLog(@"i = %li buckets = %@   %p",(long)i,str,kc_class->cache.buckets()[i]._imp);
        }
    }
    return 0;
}

打印结果 image.png 可以在元类里面找到类方法say11,印证猜想。