iOS底層原理探究07- 方法緩存cache

正題開(kāi)始之前我們先來(lái)個(gè)開(kāi)胃小菜鞏固一下之前學(xué)習(xí)的內(nèi)容

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

這段代碼輸出會(huì)是咋樣呢


image.png

輸出的結(jié)果是1 0 0 0 1 1 1 1下面的四個(gè)都是1這個(gè)應(yīng)該沒(méi)什么問(wèn)題咱們平常開(kāi)發(fā)中經(jīng)常會(huì)對(duì)對(duì)象做這樣的判斷,但是上面的四個(gè)可能就有點(diǎn)問(wèn)題了,類(lèi)的判斷不怎么做,那我們?cè)趺蠢斫膺@個(gè)結(jié)果呢?當(dāng)然是看源碼了 ,直接上源碼

+ (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;
}

通過(guò)源碼可以看到出

  • 實(shí)例方法的- (BOOL)isMemberOfClass:(Class)cls是直接判斷實(shí)例的類(lèi)跟cls相不相同,實(shí)例方法的- (BOOL)isKindOfClass:(Class)cls是比較實(shí)例的類(lèi)和它的所有父類(lèi)跟cls是否相同
  • 類(lèi)方法的+ (BOOL)isMemberOfClass:(Class)cls是直接判斷該類(lèi)的元類(lèi)跟cls相不相同,類(lèi)方法的+ (BOOL)isKindOfClass:(Class)cls是比較類(lèi)的元類(lèi)和元類(lèi)的所有父類(lèi)跟cls是否相同
    這樣來(lái)看輸出的結(jié)果就好理解了。但是這里還有一個(gè)問(wèn)題實(shí)際運(yùn)行時(shí)是否真的調(diào)用的是isMemberOfClassisKindOfClass,下面我們來(lái)看下匯編
    image.png

    通過(guò)匯編可以看到isMemberOfClass:是正常調(diào)用的,但是isKindOfClass:變成了objc_opt_isKindOfClass這個(gè)方法的調(diào)用,接下來(lái)看下這個(gè)方法的源碼
// 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);
}

其實(shí)objc_opt_isKindOfClass的核心代碼也是判斷obj的isa指向的類(lèi)以及isa指向的類(lèi)的所有父類(lèi)是否與otherClass相同。

好了開(kāi)胃小菜結(jié)束開(kāi)始正餐

一、cache的數(shù)據(jù)結(jié)構(gòu)探索

通過(guò)《iOS底層原理探究05》我們已經(jīng)了解了類(lèi)的數(shù)據(jù)結(jié)構(gòu)

struct objc_class : objc_object {
    ...省略無(wú)關(guān)代碼
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    
    ...省略無(wú)關(guān)代碼
}

bits前面的兩篇的探索中已經(jīng)探究過(guò)了,今天我們來(lái)看下class里的cache的數(shù)據(jù)結(jié)構(gòu)是怎樣的,到底存了些啥?網(wǎng)上很多博客說(shuō)cache是方法的緩存是不是這樣呢?cache的數(shù)據(jù)結(jié)構(gòu)又是咋樣的呢?下面就來(lái)探索一下。
先來(lái)看下cache_t的源碼數(shù)據(jù)結(jié)構(gòu)

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; 
    };
    ...省略無(wú)關(guān)代碼
   struct bucket_t *buckets() const;
}

主要就是一個(gè)聯(lián)合體的結(jié)構(gòu)下面在控制臺(tái)打印一下看看實(shí)際存儲(chǔ)的內(nèi)容
cache 前面是ISA、superclass這兩個(gè)指針類(lèi)型的成員變量所以拿到類(lèi)的地址往后平移16個(gè)字節(jié)(一個(gè)是8字節(jié))也就是加上0x10


image.png

來(lái)嘗試輸出cache_t里存儲(chǔ)的內(nèi)容

image.png

分別輸出 _bucketsAndMaybeMask、_maybeMask、_originalPreoptCache均沒(méi)打印我們想要的內(nèi)容,前面我們已經(jīng)有了打印方法列表和屬性列表的經(jīng)驗(yàn),一般這些數(shù)據(jù)結(jié)構(gòu)都需要使用源碼里提供的api打印,接下來(lái)看看cache_t中是否也有相應(yīng)的api,通過(guò)上面的源碼可以看到源碼中提供了struct bucket_t *buckets() const;方法來(lái)獲取buckets下面我們先來(lái)看下bucket_t的結(jié)構(gòu)再在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
...省略無(wú)關(guān)代碼
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
    }
...省略無(wú)關(guān)代碼
}

通過(guò)源碼可以看到bucket_t中存儲(chǔ)的是sel和imp,下面在控制臺(tái)里打印試試


image.png

通過(guò)buckets()方法我們確實(shí)打印出了bucket,但是打印出來(lái)的數(shù)據(jù)Value是null,這是為啥呢?這個(gè)問(wèn)題先留這個(gè)這兒,這里我們調(diào)用的buckets()既然它帶個(gè)'s'那是不是像數(shù)組一樣存多個(gè)bucket呢,我們來(lái)試一下


image.png

我們?nèi)ハ聵?biāo)為1的元素,打印出來(lái)init方法的sel和imp,由此驗(yàn)證了cache里確實(shí)存儲(chǔ)著方法的緩存。
但這里我們也產(chǎn)生了一些問(wèn)題:
  • buckets的數(shù)據(jù)結(jié)構(gòu)是咋樣的
  • 緩存方法的時(shí)候開(kāi)辟多大的空間,是怎么開(kāi)辟的
  • 方法是怎么被存到緩存里去的
    我們仿照源碼的數(shù)據(jù)結(jié)構(gòu)自己定義一套結(jié)構(gòu)方便我們繼續(xù)探索
struct sp_bucket_t {//對(duì)應(yīng)bucket_t 
    SEL _sel;
    IMP _imp;
};

struct sp_cache_t {//對(duì)應(yīng)cache_t
    struct sp_bucket_t  *buckets;
    uint32_t            _maybeMask;
    uint16_t            _flags;
    uint16_t            _occupied;
};

struct sp_class_data_bits_t {//對(duì)應(yīng)class_data_bits_t
    
    uintptr_t bits;
};

struct sp_objc_class{//對(duì)應(yīng)objc_class
 
    Class ISA;
    Class superclass;
    sp_cache_t cache;
    sp_class_data_bits_t bits;

};

定義相應(yīng)的數(shù)據(jù)結(jié)構(gòu)之后main方法中就可以添加相應(yīng)的代碼數(shù)據(jù)cache存儲(chǔ)的內(nèi)容了

        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);
  • for循環(huán)打印cache中存儲(chǔ)的方法
  • 最后打印出緩存的方法個(gè)數(shù)和開(kāi)辟的空間


    image.png
  • 當(dāng)調(diào)用say1、say2兩個(gè)方法時(shí)開(kāi)辟了三個(gè)位置,并緩存了這兩個(gè)方法,但是0號(hào)位置空出來(lái)了沒(méi)有存東西
    在加個(gè)方法試試


    image.png
  • 當(dāng)調(diào)用say1、say2、say3時(shí),cache只緩存了say3,cache開(kāi)辟了7個(gè)位置

lldb調(diào)試過(guò)程中我們遇到了一些問(wèn)題

  • 開(kāi)辟的空間和實(shí)際緩存的方法數(shù)并不一致
  • 緩存的方法并不是順序存儲(chǔ)的會(huì)有空位的情況
  • 當(dāng)開(kāi)辟的空間從3變?yōu)?的時(shí)候之前緩存的方法被清空了只剩了個(gè)say3
    帶著這些問(wèn)題我們來(lái)看源碼

cache的插入方法

//入?yún)⑹?需要緩存的方法的SEL IMP 和 方法接收者
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
    ...省略無(wú)關(guān)代碼
    // 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())) {//第一次進(jìn)來(lái)緩存是空的
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE;//計(jì)算申請(qǐng)空間的大小
        reallocate(oldCapacity, capacity, /* freeOld */false);//申請(qǐng)空間
    }
    else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {//已開(kāi)辟的空間還沒(méi)有存滿可以繼續(xù)存
        // 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 {//已開(kāi)辟的空間已經(jīng)存滿了 進(jìn)行雙倍擴(kuò)容
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        reallocate(oldCapacity, capacity, true);//開(kāi)辟空間
    }

    bucket_t *b = buckets();//取出buckets
    mask_t m = capacity - 1;//計(jì)算maybemask
    mask_t begin = cache_hash(sel, m);//使用哈希算法計(jì)算插入的位置
    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) {//判斷其他線程是否緩存過(guò)該方法
            // 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位置沒(méi)有插入成功 通過(guò)cache_next找下一個(gè)可以插入的位置

    bad_cache(receiver, (SEL)sel);//如果while循環(huán)走完都找不到可以插入的位置則緩存失敗
#endif // !DEBUG_TASK_THREADS
}

理一下緩存插入的大致的流程,過(guò)程中調(diào)用的方法在后面附上源碼

  • newOccupied = occupied() + 1這里計(jì)算插入當(dāng)前方法后緩存的方法總數(shù) newOccupied是插入當(dāng)前方法后的總數(shù) 首次進(jìn)來(lái)的時(shí)候occupied()為0 ,newOccupied = 1
  • oldCapacity = capacity()獲取之前緩存的空間大小,capacity = oldCapacity初始化新的空間大小等于老的capacity是新的空間大小
  • isConstantEmptyCache()判斷緩存是否為空 首次insert的時(shí)候緩存是空的,進(jìn)走這個(gè)流程
  • if (!capacity) capacity = INIT_CACHE_SIZE; 首次緩存capacity(需要開(kāi)辟的空間)賦值為4 (INIT_CACHE_SIZE = 4 后面附上源碼)
  • eallocate(oldCapacity, capacity, /* freeOld */false);開(kāi)辟空間
  • fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))判斷當(dāng)前開(kāi)辟的空間是否夠用
  • capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;當(dāng)前開(kāi)辟的空間不夠用的話進(jìn)行兩倍擴(kuò)容
  • 下面就是插入緩存的流程了
  • bucket_t *b = buckets();先取出buckets
  • m = capacity - 1;mayBeMask設(shè)置成開(kāi)辟的空間減一(這個(gè)后面會(huì)再看源碼)
  • begin = cache_hash(sel, m);通過(guò)哈希算法計(jì)算插入的位置,這就是方法不是順序存儲(chǔ)的原因,方法存儲(chǔ)位置的下標(biāo)是通過(guò)hash算法計(jì)算得到的
  • 進(jìn)入do-while循環(huán)把方法插入緩存fastpath(b[i].sel() == 0)判斷當(dāng)時(shí)位置是不是為空,為空則調(diào)用incrementOccupied()使_occupied + 1,通過(guò)b[i].set方法把方法插入緩存
  • b[i].sel() == sel如果之前已經(jīng)緩存過(guò)該方法直接return
  • i = cache_next(i, m)如果前面的兩個(gè)判斷都為假則查找下一個(gè)可以插入的位置
  • bad_cache(receiver, (SEL)sel)如果do-while結(jié)束也沒(méi)有找到可以插入的位置則緩存失敗

下面來(lái)看下插入流程中調(diào)用的方法的源碼實(shí)現(xiàn)

mask_t cache_t::occupied() const
{
    return _occupied;
}
  • 返回_occupied即已經(jīng)緩存的方法數(shù)
unsigned cache_t::capacity() const
{
    return mask() ? mask()+1 : 0; 
}

mask_t cache_t::mask() const
{
    return _maybeMask.load(memory_order_relaxed);
}
  • mask()返回_maybeMask即開(kāi)辟的空間大小
  • capacity()如果_maybeMask有值返回_maybeMask + 1否則返回0,因?yàn)?code>_maybeMask是等于開(kāi)辟的空間減一 所以要加回來(lái)
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;
}
  • 這個(gè)判斷是否存滿的算法是 判斷有沒(méi)有達(dá)到緩存的3/4,達(dá)到了就需要對(duì)緩存擴(kuò)容了
struct bucket_t *cache_t::buckets() const
{
    uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
    return (bucket_t *)(addr & bucketsMask);
}
  • buckets是通過(guò) _bucketsAndMaybeMask & bucketsMask得到的
  • static constexpr uintptr_t bucketsMask = ~0ul;bucketsMask等于對(duì)0取反就是所有二進(jìn)制位都是1與上_bucketsAndMaybeMask之后得到的還是_bucketsAndMaybeMask本身
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);
}
  • cache_hash算法是通過(guò)sel的地址hash得到緩存插入位置的
  • 因?yàn)榕c上了m所以得到的位置不會(huì)大于m
  • m是外界傳進(jìn)來(lái)的 m = capacity - 1 ,而capacity被初始化成INIT_CACHE_SIZE又通過(guò)INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2)INIT_CACHE_SIZE_LOG2 = 2得到INIT_CACHE_SIZE = 4m= 3 所以我們?cè)趌ldb中輸出的時(shí)候得到1 - 3的打印
    擴(kuò)容之后因?yàn)槭请p倍擴(kuò)容capacity變成了8 ,m變成7所以擴(kuò)容后打印的是1 - 7
void cache_t::incrementOccupied() 
{
    _occupied++;
}
  • 這個(gè)方法比較簡(jiǎn)單就是_occupied自增1
    未完待續(xù)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容