iOS cache_t 的散列表原理(哈希表)

iOS cache_t 的散列表原理(哈希表)

上篇我們提到,iOS 會將我們調用得方法放到我們當前的類對象的 cache_t 中,他是一個結構體,我們的方法是放在 struct bucket_t *_buckets;這個里面,他是一個散列表,以后我們調用的方法都會從這個散列表當中去查找。

散列表的原理(空間換時間)

我們來看下結構體

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


系統是怎么做的呢?

比如我們現在要去調用 -(void)test;這個方法,但我們來查找方法的時候,前面我們提到,key就是sel,也就是 @selector(test),系統是通過 key&mask 的形式來生成索引的,mask 前面有提到是當前的 buckets 的長度減1,比如我們的長度為為5,當我們調用這個方法的時候,其實是這么做的 @selector(test) & mask(4) = 索引,會生成一個索引,比如生成索引為1,那么他就會去buckets取出來1位置的結構體,這就是我們以后調用的方法的結構體,不用循環遍歷。

其實當我們調用方法的時候,比如上面的test方法,當我們要放到緩存里面的時候,系統就會通過 @selector(test) & mask 來生成一個索引,比如生成的索引為5,那么,就會直接將這個方法放到索引為5的位置里面,這樣在我們取得時候,就可以精確的取出來了,那么前面的位置怎么辦?前面的位置,系統會將 null 放進去,是不是很巧妙?加快了查找效率。

上面這種做法其實是空間換時間的一種方法,但是我們可以知道,通過這個 & mask,的運算,最后得到的索引值,一定是小于等于 mask 的值得,不信自己可以測試下看看。

經過運算之后,生成的索引一樣怎么辦?

這段代碼為源碼


bucket_t * cache_t::find(SEL s, id receiver)
{
    assert(s != 0);

    bucket_t *b = buckets();
    mask_t m = mask();
    mask_t begin = cache_hash(s, m);
    mask_t i = begin;
    do {
        if (b[i].sel() == 0  ||  b[i].sel() == s) {
            return &b[i];
        }
    } while ((i = cache_next(i, m)) != begin);

    // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)s, cls);
}

這段代碼什么意思呢?前面都能明白,我們直接看 dowhile就可以了,從代碼中可以看到,當我們通過 hash 算法,算出一個index之后,如果我們的散列表中的這個位置為空或者這個位置的sel和我們傳進來的sel一樣,那么直接返回散列表中的這個位置的結構體,如果沒找到相同的,也就是說,可能當前別的方法和我們mask進行hash運算生成的index形成了沖突,這個位置的方法,不是我們當前傳進來的方法,那么怎么辦呢?看源碼


static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}

我們通過查看 cache_next 方法,這個方法一看就知道了,比如我們這時候 i 為 4,發現沖突,那么直接返回 i - 1,將新傳進來的方法放到 3 的位置,如果減到0還找不到,那么就放到 mask 位置,也就是數組長度的最大值,相當于 append ,然后再重新查找,當我們 mask 的值發生變化的時候,比如擴容,系統會為我們擴容,并清空我們的散列表,重新來,擴容,相當于當前的容量 * 2,一開始我們的散列表是有預設值得,比如一開始上來,長度就為10,先用,發現不夠,變成20,清空,擴容。

hash 表的原理其實就是 f(key)=index,給定一個值生成一個索引。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容