方法緩存cache_t 探究

類(lèi)結(jié)構(gòu)分析中怎燥,只看了大致看了一下cache的基本結(jié)構(gòu),接下來(lái)我來(lái)深入了解一下cache_t在類(lèi)對(duì)象中的作用蜜暑。

cache_t的結(jié)構(gòu)

//簡(jiǎn)化后的cache_t
struct cache_t {
    struct bucket_t *_buckets; 
    mask_t _mask; 
    mask_t _occupied;
}

//簡(jiǎn)化后的 bucket_t
//存儲(chǔ)著方法實(shí)現(xiàn)imp 和 緩存的key
struct bucket_t {
    MethodCacheIMP _imp;
    cache_key_t _key; 
};
  • _buckets 是一個(gè)緩存方法的散列表
  • _mask 表示散列表長(zhǎng)度 - 1
  • _occupied表示已經(jīng)占用數(shù)量

cache_t中的函數(shù)分析

前面只看了cache_t的結(jié)構(gòu)铐姚,接下來(lái)看一下它提供有哪些函數(shù)

struct cache_t {
    struct bucket_t *_buckets; 
    mask_t _mask; 
    mask_t _occupied;

public:
    struct bucket_t *buckets();
    mask_t mask();
    mask_t occupied();
    void incrementOccupied();
    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
    void initializeToEmpty();

    mask_t capacity();
    bool isConstantEmptyCache();
    bool canBeFreed();

    static size_t bytesForCapacity(uint32_t cap);
    static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);

    void expand();
    void reallocate(mask_t oldCapacity, mask_t newCapacity);
    struct bucket_t * find(cache_key_t key, id receiver);

    static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn));
};
  1. buckets()
    這個(gè)方法的實(shí)現(xiàn)很簡(jiǎn)單就是_buckets對(duì)外的一個(gè)獲取函數(shù)
  2. mask()
    獲取緩存容量_mask
  3. occupied()
    獲取已經(jīng)占用的緩存?zhèn)€數(shù)_occupied
  4. incrementOccupied()
    增加緩存,_occupied自++
void cache_t::incrementOccupied() 
{
    _occupied++;
}
  1. setBucketsAndMask(,)
    這個(gè)函數(shù)是設(shè)置一個(gè)新的Buckets
void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
    _buckets = newBuckets; //設(shè)置新的_buckets 
    _mask = newMask; //設(shè)置新buckets的容量
    _occupied = 0; //新buckets默認(rèn)占用為0
}
  1. initializeToEmpty()
    初始化cache并設(shè)置為空
void cache_t::initializeToEmpty()
{
    bzero(this, sizeof(*this)); //將cache 所有內(nèi)容都抹除 (將所有cache的空間都填充為'\0')
    _buckets = (bucket_t *)&_objc_empty_cache; //把_buckets指向cache empty的實(shí)現(xiàn)
}

_objc_empty_cache 為已經(jīng)提供了的空cache

struct objc_cache _objc_empty_cache =
{
    0,        // mask
    0,        // occupied
    { NULL }  // buckets
};
  1. capacity()
//獲取buckets的容量
mask_t cache_t::capacity() 
{
    return mask() ? mask()+1 : 0; 
}

capacity()經(jīng)過(guò)了出來(lái)肛捍,當(dāng)mask() == 0時(shí),返回0隐绵;當(dāng)mask() > 0時(shí),返回mask() + 1

  • mask() == 0 表示了buckets為空的狀態(tài)
  • mask() > 0 表示了buckets已經(jīng)存在緩存

那什么需要mask() + 1 ?
擴(kuò)容算法需要:expand()中的擴(kuò)容算法基本邏輯(最小分配的容量是4拙毫,當(dāng)容量存滿3/4時(shí)依许,進(jìn)行擴(kuò)容,擴(kuò)容當(dāng)前容量的兩倍)缀蹄;這樣最小容量4的 1/4就是1峭跳,這就是mask() + 1的原因。

  1. isConstantEmptyCache()
    判斷buckets是否為空

  2. canBeFreed()
    isConstantEmptyCache()取反缺前,表示buckets不為空可以釋放

  3. bytesForCapacity()

  4. expand()

void cache_t::expand()
{
    //拿到原有的buckets容量
    uint32_t oldCapacity = capacity(); 
    
    /*
    計(jì)算新buckets的容量
    INIT_CACHE_SIZE = 4
    如果 oldCapacity == 0蛀醉,則使用最小容量4 
    如果 oldCapacity > 0,則擴(kuò)容兩倍
    */
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;

    if ((uint32_t)(mask_t)newCapacity != newCapacity) {
        // mask overflow - can't grow further
        // fixme this wastes one bit of mask
        newCapacity = oldCapacity;
    }
    
    //重新分配
    reallocate(oldCapacity, newCapacity);
}
  1. reallocate()
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
    bool freeOld = canBeFreed();
    //拿到原有buckets
    bucket_t *oldBuckets = buckets();
    //創(chuàng)建一個(gè)新的buckets
    bucket_t *newBuckets = allocateBuckets(newCapacity);
    
    //設(shè)置新的buckets 和 mask(capacity - 1)
    setBucketsAndMask(newBuckets, newCapacity - 1);
    
    //抹掉原有buckets的數(shù)據(jù)
    if (freeOld) {
        cache_collect_free(oldBuckets, oldCapacity);
        cache_collect(false);
    }
}
  • 為什么要?jiǎng)?chuàng)建新的新的buckets來(lái)替換原有的buckets并抹掉原有的buckets的方案诡延,而不是在在原有buckets的基礎(chǔ)上進(jìn)行擴(kuò)容滞欠?
  1. 減少對(duì)方法快速查找流程的影響:調(diào)用objc_msgSend時(shí)會(huì)觸發(fā)方法快速查找,如果進(jìn)行擴(kuò)容需要做一些讀寫(xiě)操作肆良,對(duì)快速查找影響比較大筛璧。
  2. 對(duì)性能要求比較高:開(kāi)辟新的buckets空間并抹掉原有buckets的消耗比在原有buckets上進(jìn)行擴(kuò)展更加高效
  1. find()
bucket_t * cache_t::find(cache_key_t k, id receiver)
{
    bucket_t *b = buckets();
    mask_t m = mask();
    //計(jì)算 key 的 index
    mask_t begin = cache_hash(k, m);
    mask_t i = begin;
    do {
        //key = 0 ,表示i索引的bucket 還沒(méi)有緩存方法惹恃,返回bucket 中止查找
        //key = k, 表示查詢成功夭谤,返回該bucket
        if (b[i].key() == 0  ||  b[i].key() == k) {
            return &b[i];
        }
    } while ((i = cache_next(i, m)) != begin); //當(dāng)出現(xiàn) 當(dāng)出現(xiàn)hash碰撞 cache_next 查找下一個(gè),直到回到begin

    // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)k, cls);
}
  • 通過(guò) cache_key_t 查找receiver中的 bucket_t *
  • 如果碰到hash 沖突,則通過(guò)cache_next 偏移
  • cache_next 把散列表的頭尾相連 ( (i +1) % mask)

Hash

static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
{
    //取余法計(jì)算索引
    return (mask_t)(key & mask);
}
  • key 就是 SEL
  • 映射關(guān)系其實(shí)就是 key & mask = index
  • mask = 散列表長(zhǎng)度 - 1
  • 所以 index 一定是 <= mask

Hash表的原理: f(key) = index

Hash碰撞

兩個(gè)方法key & mask,是完全有可能計(jì)算出相同的結(jié)果巫糙,這里是通過(guò)這個(gè)函數(shù)處理

// do {  } while ((i = cache_next(i, m)) != begin);
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}
  • 當(dāng)發(fā)生hash碰撞就查找下一個(gè)朗儒,cache_next就提供了這種方法,每次 +1 當(dāng) (i+1) = mask時(shí)参淹,因?yàn)橛?& mask 所以索引i = 0又回到了散列表頭部醉锄。
  • 這樣就會(huì)把散列表頭尾連接起來(lái)形成一個(gè)環(huán)。

什么時(shí)候存儲(chǔ)到cache中

objc_msgSend第一次發(fā)送消息會(huì)觸發(fā)方法查找浙值,找到方法后會(huì)調(diào)用cache_fill()方法把方法緩存到cache中

cache添加緩存的核心代碼
//方法查找后會(huì)調(diào)該方法來(lái)緩存方法
void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
    //這里省略了 lock的代碼
    //填充cache
    cache_fill_nolock(cls, sel, imp, receiver);
}

static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
    // 如果能找到緩存就直接返回恳不,確保沒(méi)有其它線程把方法加入到cache中
    if (cache_getImp(cls, sel)) return;

    //獲取cls的cache
    cache_t *cache = getCache(cls);
    //換算出sel的key
    cache_key_t key = getKey(sel);

    //加上即將加入緩存的占用數(shù)
    mask_t newOccupied = cache->occupied() + 1;
    //拿到當(dāng)前buckets的容量
    mask_t capacity = cache->capacity();
    if (cache->isConstantEmptyCache()) {
        /*
          當(dāng)cache為空時(shí),則重新分配空間开呐;
          當(dāng) capacity == 0時(shí) 烟勋,使用最小的緩存空間 INIT_CACHE_SIZE = 4
        */
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {
        // Cache is less than 3/4 full. Use it as-is.
        // 使用的空間 newOccupied < 3/4, 不需要擴(kuò)容
    }
    else {
        // Cache is too full. Expand it.
        // 使用的空間 newOccupied >= 3/4, 對(duì)cache進(jìn)行擴(kuò)容
        cache->expand();
    }

    //find 使用hash找到可用的bucket指針
    bucket_t *bucket = cache->find(key, receiver);
    //判斷 bucket 是否可用,如果可用對(duì)齊occupied +1
    if (bucket->key() == 0) cache->incrementOccupied();
    //把緩存方法放到bucket中
    bucket->set(key, imp);
}

總結(jié)

  1. mask = 散列表長(zhǎng)度 - 1
  2. 除了Empty筐付,最小的buckets大小是 4(為了支持?jǐn)U容算法需要)
  3. cache 擴(kuò)容策略是 >= 3/4,擴(kuò)容兩倍
  4. cache擴(kuò)容是創(chuàng)建一個(gè)更大的buckets卵惦,替換原有的buckets
  5. 替換策略為了讓方法快速查找更加安全穩(wěn)定和提高效率
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市瓦戚,隨后出現(xiàn)的幾起案子沮尿,更是在濱河造成了極大的恐慌,老刑警劉巖较解,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件畜疾,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡哨坪,警方通過(guò)查閱死者的電腦和手機(jī)庸疾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)当编,“玉大人届慈,你說(shuō)我怎么就攤上這事》尥担” “怎么了金顿?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)鲤桥。 經(jīng)常有香客問(wèn)我揍拆,道長(zhǎng),這世上最難降的妖魔是什么茶凳? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任嫂拴,我火速辦了婚禮播揪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘筒狠。我一直安慰自己猪狈,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布辩恼。 她就那樣靜靜地躺著雇庙,像睡著了一般。 火紅的嫁衣襯著肌膚如雪灶伊。 梳的紋絲不亂的頭發(fā)上疆前,一...
    開(kāi)封第一講書(shū)人閱讀 52,457評(píng)論 1 311
  • 那天,我揣著相機(jī)與錄音聘萨,去河邊找鬼竹椒。 笑死,一個(gè)胖子當(dāng)著我的面吹牛匈挖,可吹牛的內(nèi)容都是我干的碾牌。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼儡循,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼舶吗!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起择膝,我...
    開(kāi)封第一講書(shū)人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤誓琼,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后肴捉,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體腹侣,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年齿穗,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了傲隶。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡窃页,死狀恐怖跺株,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情脖卖,我是刑警寧澤乒省,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站畦木,受9級(jí)特大地震影響袖扛,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜十籍,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一蛆封、第九天 我趴在偏房一處隱蔽的房頂上張望唇礁。 院中可真熱鬧,春花似錦娶吞、人聲如沸垒迂。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至楷拳,卻和暖如春绣夺,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背欢揖。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工陶耍, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人她混。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓烈钞,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親坤按。 傳聞我的和親對(duì)象是個(gè)殘疾皇子毯欣,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360