iOS開(kāi)發(fā)cache_t(緩存)

??在上一篇文章里iOS開(kāi)發(fā)之類(lèi)的本質(zhì)里,我們?cè)敿?xì)研究了bits庄岖,我們用內(nèi)存偏移得出的豁翎,我們計(jì)算了cache_t的大小,然后用lldb打印出了bits里面的內(nèi)容隅忿。今天心剥,我們來(lái)研究,我們跳過(guò)的背桐,cache_t里面究竟存放了什么東西优烧。我們先來(lái)進(jìn)去看看cache_t的結(jié)構(gòu):

struct cache_t {
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    explicit_atomic<struct bucket_t *> _buckets;
    explicit_atomic<mask_t> _mask;
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
    static constexpr uintptr_t maskShift = 48;
    static constexpr uintptr_t maskZeroBits = 4;
    static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
    static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1;
    static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS,
    "Bucket field doesn't have enough bits for arbitrary pointers.");
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4

    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
    #if __LP64__
    uint16_t _flags;
    #endif
    uint16_t _occupied;
}

這里有不同的架構(gòu)處理方式:

  • CACHE_MASK_STORAGE_OUTLINED表示macOSi386的架構(gòu)链峭。
  • CACHE_MASK_STORAGE_HIGH_16表示真機(jī)畦娄,arm64的架構(gòu)。
  • CACHE_MASK_STORAGE_LOW_4表示模擬器,x86的架構(gòu)熙卡。
    我們繼續(xù)查看里面的bucket_t源碼杖刷,里面有兩個(gè)版本,真機(jī)和非真機(jī)驳癌,只是selimp的順序不同
struct bucket_t {
private:
#if __arm64__ //真機(jī)
    //explicit_atomic 是加了原子性的保護(hù)
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else //非真機(jī)
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif
    //方法等其他部分省略
}

現(xiàn)在我們查找cache中的sel和imp滑燃,我們先定義一些方法和屬性,并實(shí)現(xiàn)喂柒。

@interface Person : NSObject{
    NSString *name;
}
@property (nonatomic, copy)NSString *nickname;
@property (nonatomic, copy)NSString *height;
-(void)eat;
-(void)drink;
-(void)say;
+(void)run;
@end

然后我們調(diào)用

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...  class_data_bits_t
        Person *person = [Person alloc];
        
        [person eat];
        [person say];
        [person drink];
        [Person run];
        NSLog(@"Hello, World!");
    }
    return 0;
}

添加斷點(diǎn)


接下來(lái)又是我們熟悉的lldb調(diào)試:

  • p $3.buckets()是因?yàn)槲覀冊(cè)?code>cache_t的源碼里不瓶,提供了這個(gè)方法:

    同樣的,p $5.sel()/p $5.imp(p)也是cache_t的源碼里提供的方法:

通過(guò)我們上面的調(diào)試灾杰,我們看到蚊丐,第一次斷點(diǎn)的時(shí)候,cache_t的緩存中艳吠,_occupied = 0麦备,在第二次斷點(diǎn)的時(shí)候,我們看到了昭娩,里面有了方法的緩存凛篙,然后我們打印出了eat()方法。我們?cè)賵?zhí)行一步方法栏渺,然后看看緩存中是否多了內(nèi)容呛梆。


可以看到,在執(zhí)行了say()方法后磕诊,我們成功的緩存中打印出了say()方法填物。這里p($9+1)就用到了我們之前說(shuō)到的指針偏移。你也可以p $8.buckets()[0]/p $8.buckets()[1]

脫離源碼環(huán)境通過(guò)項(xiàng)目查找

??我們之前的調(diào)試都是通過(guò)源碼環(huán)境進(jìn)行的霎终,那么我們能不能脫離源碼環(huán)境來(lái)進(jìn)行查找呢滞磺?我們可以模擬源碼環(huán)境。然后將需要的源碼進(jìn)行設(shè)計(jì)莱褒,拷貝到項(xiàng)目中击困,模擬一下方法的寫(xiě)入流程:

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

struct hk_bucket_t {
    SEL _sel;
    IMP _imp;
};

struct hk_cache_t {
    struct hk_bucket_t * _buckets;
    mask_t _mask;
    uint16_t _flags;
    uint16_t _occupied;
};

struct hk_class_data_bits_t {
    uintptr_t bits;
};

struct lg_objc_class {
    Class ISA;
    Class superclass;
    struct hk_cache_t cache;             // formerly cache pointer and vtable
    struct hk_class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
};
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...  class_data_bits_t _buckets
        Person *person = [Person alloc];
        Class p = [Person class];
        
        [person say1];
        [person say2];
//        [person say3];
//        [person say4];
//        
        struct lg_objc_class *hk_pClass = (__bridge struct lg_objc_class *)(p);
        NSLog(@"%hu - %u",hk_pClass->cache._occupied,hk_pClass->cache._mask);
        for (mask_t i = 0; i < hk_pClass->cache._mask; i++) {
            // 打印獲取的 bucket
            struct hk_bucket_t bucket = hk_pClass->cache._buckets[i];
            NSLog(@"%@ - %p",NSStringFromSelector(bucket._sel),bucket._imp);
        }
        
        NSLog(@"Hello, World!%@",p);
    }
    return 0;
}

看一下輸出結(jié)果:


然后我們打開(kāi)注釋的say3()say4()方法,看看打印結(jié)果:

發(fā)現(xiàn)有變化的是_occupied广凸,_mask阅茶,從2-3變成了2-7,那么他們分別是什么意思呢炮障?并且say3(),say4()的調(diào)用順序貌似有問(wèn)題目派,和我們調(diào)用的順序不太一樣。我們看cache_t源碼里的方法:

然后我們?nèi)炙阉鬟@個(gè)方法,我們找到了cache_t的插入方法:

void cache_t::insert(Class cls, 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 <= capacity / 4 * 3)) {
        // Cache is less than 3/4 full. Use it as-is.
    }
    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;
do {
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();
            b[i].set<Atomic, Encoded>(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));
}
  • 如果有內(nèi)容插入就會(huì)調(diào)用代碼:mask_t newOccupied = occupied() + 1;
  • 然后進(jìn)入判斷胁赢,capacity = INIT_CACHE_SIZEINIT_CACHE_SIZE在這里等于4,然后開(kāi)辟空間函數(shù)reallocate()
    然后我們看一下這個(gè)函數(shù):
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
    bucket_t *oldBuckets = buckets();
    bucket_t *newBuckets = allocateBuckets(newCapacity);
    ASSERT(newCapacity > 0);
    ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);

    setBucketsAndMask(newBuckets, newCapacity - 1);
    
    if (freeOld) {
        cache_collect_free(oldBuckets, oldCapacity);
    }
}
  • 里面有個(gè)寫(xiě)入的函數(shù)allocateBuckets()智末,再往里面就是我們熟悉的calloc函數(shù)谅摄,然后還有個(gè)函數(shù)setBucketsAndMask(),這個(gè)函數(shù)的作用是為了寫(xiě)入cache_t系馆。
  • 我們繼續(xù)流程分析送漠,往下繼續(xù)判斷,// Cache is less than 3/4 full. Use it as-is.就是如果大于3/4由蘑,就會(huì)進(jìn)行擴(kuò)容闽寡,我們一開(kāi)始申請(qǐng)了4個(gè),然后到第3個(gè)的時(shí)候尼酿,我們就會(huì)*2爷狈,所以我們繼續(xù)往下看到了mask_t m = capacity - 1;,這就是為什么我們打印出來(lái)的mask是3和7了裳擎,因?yàn)?code>3 = 4-1涎永,7 = 4*2-1
  • 在擴(kuò)容算法里,我們有個(gè)reallocate(oldCapacity, capacity, true);函數(shù)鹿响,跟上面的一樣羡微,里面有個(gè)判斷,freeold惶我,然后調(diào)用了cache_collect_free(oldBuckets, oldCapacity);妈倔,我們繼續(xù)看一下具體實(shí)現(xiàn):
static void cache_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_collect(false);
}

里面有個(gè)_garbage_make_room ();函數(shù),繼續(xù)往里看:

static void _garbage_make_room(void)
{
    static int first = 1;

    // Create the collection table the first time it is needed
    if (first)
    {
        first = 0;
        garbage_refs = (bucket_t**)
            malloc(INIT_GARBAGE_COUNT * sizeof(void *));
        garbage_max = INIT_GARBAGE_COUNT;
    }

    // Double the table if it is full
    else if (garbage_count == garbage_max)
    {
        garbage_refs = (bucket_t**)
            realloc(garbage_refs, garbage_max * 2 * sizeof(void *));
        garbage_max *= 2;
    }
}

這里才找到了真的擴(kuò)容函數(shù)绸贡,所做的事情盯蝴,就是重新申請(qǐng)內(nèi)存空間realloc(garbage_refs, garbage_max * 2 * sizeof(void *));``garbage_max = INIT_GARBAGE_COUNT;``INIT_GARBAGE_COUNT = 128

總結(jié):
1恃轩、_mask是什么结洼?

_mask是指掩碼數(shù)據(jù),用于在哈希算法或者哈希沖突算法中計(jì)算哈希下標(biāo)叉跛,其中mask等于capacity - 1

2松忍、_occupied 是什么?

_occupied表示哈希表中sel-imp的占用大小 (即可以理解為分配的內(nèi)存中已經(jīng)存儲(chǔ)了sel-imp的的個(gè)數(shù))筷厘,所有的方法調(diào)用都會(huì)影響_occupied,包括init方法鸣峭。

4、bucket數(shù)據(jù)為什么會(huì)有丟失的情況酥艳?摊溶,例如2-7中,只有say3充石、say4方法有函數(shù)指針

原因是在擴(kuò)容時(shí)莫换,是將原有的內(nèi)存全部清除了,再重新申請(qǐng)了內(nèi)存導(dǎo)致的

5、2-7中say3拉岁、say4的打印順序?yàn)槭裁词莝ay4先打印坷剧,say3后打印,且還是挨著的喊暖,即順序有問(wèn)題惫企?

因?yàn)?code>sel-imp的存儲(chǔ)是通過(guò)哈希算法計(jì)算下標(biāo)的,其計(jì)算的下標(biāo)有可能已經(jīng)存儲(chǔ)了sel陵叽,所以又需要通過(guò)哈希沖突算法重新計(jì)算哈希下標(biāo)狞尔,所以導(dǎo)致下標(biāo)是隨機(jī)的,并不是固定的

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末巩掺,一起剝皮案震驚了整個(gè)濱河市偏序,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌锌半,老刑警劉巖禽车,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異刊殉,居然都是意外死亡殉摔,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)记焊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)逸月,“玉大人,你說(shuō)我怎么就攤上這事遍膜⊥胗玻” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵瓢颅,是天一觀的道長(zhǎng)恩尾。 經(jīng)常有香客問(wèn)我,道長(zhǎng)挽懦,這世上最難降的妖魔是什么翰意? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮信柿,結(jié)果婚禮上冀偶,老公的妹妹穿的比我還像新娘。我一直安慰自己渔嚷,他們只是感情好进鸠,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著形病,像睡著了一般客年。 火紅的嫁衣襯著肌膚如雪霞幅。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,541評(píng)論 1 305
  • 那天搀罢,我揣著相機(jī)與錄音蝗岖,去河邊找鬼侥猩。 笑死榔至,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的欺劳。 我是一名探鬼主播唧取,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼划提!你這毒婦竟也來(lái)了枫弟?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤鹏往,失蹤者是張志新(化名)和其女友劉穎淡诗,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體伊履,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡韩容,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了唐瀑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片群凶。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖哄辣,靈堂內(nèi)的尸體忽然破棺而出请梢,到底是詐尸還是另有隱情,我是刑警寧澤力穗,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布毅弧,位于F島的核電站,受9級(jí)特大地震影響当窗,放射性物質(zhì)發(fā)生泄漏够坐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一超全、第九天 我趴在偏房一處隱蔽的房頂上張望咆霜。 院中可真熱鬧,春花似錦嘶朱、人聲如沸蛾坯。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)脉课。三九已至救军,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間倘零,已是汗流浹背唱遭。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留呈驶,地道東北人拷泽。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像袖瞻,于是被迫代替她去往敵國(guó)和親司致。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355