iOS底层原理探究07- 方法缓存cache

2021-09-24  本文已影响0人  superFool

正题开始之前我们先来个开胃小菜巩固一下之前学习的内容

    BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];       //
    BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];     //
    BOOL re3 = [(id)[LGPerson class] isKindOfClass:[LGPerson class]];       //
    BOOL re4 = [(id)[LGPerson class] isMemberOfClass:[LGPerson class]];     //
    NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);

    BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];       //
    BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];     //
    BOOL re7 = [(id)[LGPerson alloc] isKindOfClass:[LGPerson class]];       //
    BOOL re8 = [(id)[LGPerson alloc] isMemberOfClass:[LGPerson class]];     //
    NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);

这段代码输出会是咋样呢


image.png

输出的结果是1 0 0 0 1 1 1 1下面的四个都是1这个应该没什么问题咱们平常开发中经常会对对象做这样的判断,但是上面的四个可能就有点问题了,类的判断不怎么做,那我们怎么理解这个结果呢?当然是看源码了 ,直接上源码

+ (BOOL)isMemberOfClass:(Class)cls {
    return self->ISA() == cls;
}

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}

+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) {
        if (tcls == cls) return YES;
    }
    return NO;
}

- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->getSuperclass()) {
        if (tcls == cls) return YES;
    }
    return NO;
}

通过源码可以看到出

// Calls [obj isKindOfClass]
BOOL
objc_opt_isKindOfClass(id obj, Class otherClass)
{
#if __OBJC2__
    if (slowpath(!obj)) return NO;
    Class cls = obj->getIsa();
    if (fastpath(!cls->hasCustomCore())) {
        for (Class tcls = cls; tcls; tcls = tcls->getSuperclass()) {
            if (tcls == otherClass) return YES;
        }
        return NO;
    }
#endif
    return ((BOOL(*)(id, SEL, Class))objc_msgSend)(obj, @selector(isKindOfClass:), otherClass);
}

其实objc_opt_isKindOfClass的核心代码也是判断obj的isa指向的类以及isa指向的类的所有父类是否与otherClass相同。

好了开胃小菜结束开始正餐

一、cache的数据结构探索

通过《iOS底层原理探究05》我们已经了解了类的数据结构

struct objc_class : objc_object {
    ...省略无关代码
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    
    ...省略无关代码
}

bits前面的两篇的探索中已经探究过了,今天我们来看下class里的cache的数据结构是怎样的,到底存了些啥?网上很多博客说cache是方法的缓存是不是这样呢?cache的数据结构又是咋样的呢?下面就来探索一下。
先来看下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; 
    };
    ...省略无关代码
   struct bucket_t *buckets() const;
}

主要就是一个联合体的结构下面在控制台打印一下看看实际存储的内容
cache 前面是ISA、superclass这两个指针类型的成员变量所以拿到类的地址往后平移16个字节(一个是8字节)也就是加上0x10


image.png

来尝试输出cache_t里存储的内容

image.png
分别输出 _bucketsAndMaybeMask、_maybeMask、_originalPreoptCache均没打印我们想要的内容,前面我们已经有了打印方法列表和属性列表的经验,一般这些数据结构都需要使用源码里提供的api打印,接下来看看cache_t中是否也有相应的api,通过上面的源码可以看到源码中提供了struct bucket_t *buckets() const;方法来获取buckets下面我们先来看下bucket_t的结构再在lldb中打印一下
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
...省略无关代码
inline SEL sel() const { return _sel.load(memory_order_relaxed); }
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
    }
...省略无关代码
}

通过源码可以看到bucket_t中存储的是sel和imp,下面在控制台里打印试试


image.png

通过buckets()方法我们确实打印出了bucket,但是打印出来的数据Value是null,这是为啥呢?这个问题先留这个这儿,这里我们调用的buckets()既然它带个's'那是不是像数组一样存多个bucket呢,我们来试一下


image.png
我们去下标为1的元素,打印出来init方法的sel和imp,由此验证了cache里确实存储着方法的缓存。
但这里我们也产生了一些问题:
struct sp_bucket_t {//对应bucket_t 
    SEL _sel;
    IMP _imp;
};

struct sp_cache_t {//对应cache_t
    struct sp_bucket_t  *buckets;
    uint32_t            _maybeMask;
    uint16_t            _flags;
    uint16_t            _occupied;
};

struct sp_class_data_bits_t {//对应class_data_bits_t
    
    uintptr_t bits;
};

struct sp_objc_class{//对应objc_class
 
    Class ISA;
    Class superclass;
    sp_cache_t cache;
    sp_class_data_bits_t bits;

};

定义相应的数据结构之后main方法中就可以添加相应的代码数据cache存储的内容了

        struct sp_objc_class *sp_class = (__bridge struct sp_objc_class *)(pClass);
        
        for (uint32_t i = 0; i < sp_class->cache._maybeMask; i++) {
            struct sp_bucket_t bucket = sp_class->cache.buckets[I];
            NSLog(@"%@ - %p",NSStringFromSelector(bucket._sel),bucket._imp);
        }
        NSLog(@"%hu - %u ",sp_class->cache._occupied,sp_class->cache._maybeMask);

lldb调试过程中我们遇到了一些问题

cache的插入方法

//入参是 需要缓存的方法的SEL IMP 和 方法接收者
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;//计算申请空间的大小
        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();//取出buckets
    mask_t m = capacity - 1;//计算maybemask
    mask_t begin = cache_hash(sel, m);//使用哈希算法计算插入的位置
    mask_t i = begin;//I表示插入位置

    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot.
    do {
        if (fastpath(b[i].sel() == 0)) {//如果插入位置是空的 则插入该方法(避免hash冲突)
            incrementOccupied();
            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));//如果i位置没有插入成功 通过cache_next找下一个可以插入的位置

    bad_cache(receiver, (SEL)sel);//如果while循环走完都找不到可以插入的位置则缓存失败
#endif // !DEBUG_TASK_THREADS
}

理一下缓存插入的大致的流程,过程中调用的方法在后面附上源码

下面来看下插入流程中调用的方法的源码实现

mask_t cache_t::occupied() const
{
    return _occupied;
}
unsigned cache_t::capacity() const
{
    return mask() ? mask()+1 : 0; 
}

mask_t cache_t::mask() const
{
    return _maybeMask.load(memory_order_relaxed);
}
bool cache_t::isConstantEmptyCache() const
{
    return
        occupied() == 0  &&
        buckets() == emptyBucketsForCapacity(capacity(), false);
}
static inline mask_t cache_fill_ratio(mask_t capacity) {
    return capacity * 3 / 4;
}
struct bucket_t *cache_t::buckets() const
{
    uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
    return (bucket_t *)(addr & bucketsMask);
}
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);
}
void cache_t::incrementOccupied() 
{
    _occupied++;
}
上一篇 下一篇

猜你喜欢

热点阅读