記一次LFU緩存實(shí)現(xiàn)

這篇文章大多是我自己的基于iOS開(kāi)發(fā)一個(gè)想法過(guò)程,實(shí)現(xiàn)并不難涨颜。不過(guò)我并不會(huì)貼出全部代碼项滑,天知道我組織文章的根據(jù)是什么依沮,能看懂都是緣分。

前言

討論緩存時(shí)枪狂,都會(huì)分內(nèi)存緩存和硬盤(pán)緩存危喉,本文主要是基于內(nèi)存緩存的,但是完全可以稍加改動(dòng)便可以應(yīng)用于硬盤(pán)緩存中州疾。在做其他文件IO的時(shí)辜限,會(huì)有很多情況會(huì)基于C⊙媳停基于此列粪,我選擇了使用C來(lái)實(shí)現(xiàn)核心部分。而它的作用是主要用于WebView在內(nèi)存中的緩存數(shù)據(jù)谈飒。

關(guān)于LFU的一些概念之類(lèi)的東西就不說(shuō)了,原理實(shí)現(xiàn)網(wǎng)上一大堆态蒂『即耄基于自己?jiǎn)捂湵硗ㄟ^(guò)棧來(lái)實(shí)現(xiàn)LFU的方式實(shí)現(xiàn)一下,這不一定是最優(yōu)解钾恢,也有可能是最差解手素。不管怎么說(shuō)鸳址,先通過(guò)一個(gè)圖闡述一下主要的數(shù)據(jù)結(jié)構(gòu):


棧數(shù)據(jù)變化

多想一點(diǎn)兒東西

在看了上圖之后,首先的想法就是去把具體算法擼出來(lái)泉懦。但是在開(kāi)動(dòng)寫(xiě)代碼之前稿黍,我們需要想的東西應(yīng)該要更多。在面向過(guò)程的思想中想開(kāi)來(lái)崩哩,使用棧的時(shí)候取出操作可以很快速的完成最快可以達(dá)到時(shí)間復(fù)雜度為O(1)巡球,而在維護(hù)棧順序的時(shí)候,最壞的時(shí)間復(fù)雜度為O(N)邓嘹,其查找和位置更新是在同一個(gè)循環(huán)中完成的酣栈,避免了不必要的時(shí)間消耗。

加入到面向?qū)ο笾?/h3>

顯然用C來(lái)做業(yè)務(wù)邏輯開(kāi)發(fā)始終有點(diǎn)兒蹩腳汹押,所以得引入到面向?qū)ο笾锌篌荨N野堰@個(gè)類(lèi)取名叫做PPWebFetchCache吧,既然都面向?qū)ο罅伺锛郑辉倥c(diǎn)兒設(shè)計(jì)模式進(jìn)去窖维?考慮到易于操作性,對(duì)于PPWebFetchCache類(lèi)妙痹,我做了一個(gè)單例铸史,當(dāng)然也可以自己生成一個(gè)對(duì)象。到了這里下面應(yīng)該要想的就是對(duì)象的屬性和操作方法等等事情细诸,但是自己想著就用了一個(gè)單例模式這不是表明我設(shè)計(jì)模式很low嗎沛贪?不過(guò)事實(shí)的確是這樣,我沒(méi)有什么好的設(shè)計(jì)模式拿出來(lái)~~~
所以我就硬塞了一個(gè)簡(jiǎn)單工廠模式進(jìn)去震贵。它做什么呢利赋?我想的是“現(xiàn)在只是做LFU,萬(wàn)一哪一天變化來(lái)了猩系,讓我用寫(xiě)一個(gè)LRU的緩存策略媚送,那我不是死得很慘!”寇甸,所以我又創(chuàng)建了一個(gè)繼承于PPWebFetchCache類(lèi)的PPWebFetchLFUCache塘偎,和另一個(gè)用于將來(lái)實(shí)現(xiàn)LRU算法的PPWebFetchLRUCache類(lèi)。并在最后給父類(lèi)添加了屬性maxMemoryCost拿霉、maxMemoryCountLimit和兩個(gè)操作方法storeCacheInMemory吟秩、memoryCacheWithURL。

走進(jìn)實(shí)現(xiàn)細(xì)節(jié)

上面說(shuō)的LFU是針對(duì)于某一個(gè)URL的使用次數(shù)绽淘。在思考如何使用最少時(shí)間拿到URL對(duì)應(yīng)的數(shù)據(jù)時(shí)涵防,顯然散列表最理想的數(shù)據(jù)結(jié)構(gòu),而且在iOS中用散列表實(shí)現(xiàn)的代表便是Dictionary沪铭。所以大致的邏輯是:

使用NSDictionary來(lái)實(shí)現(xiàn)URL和數(shù)據(jù)一對(duì)一在內(nèi)存中的存儲(chǔ)壮池。而LFU主要用于管理某一URL的使用次數(shù)偏瓤,淘汰掉使用次數(shù)最少的URL,并在內(nèi)存字典中刪除對(duì)應(yīng)的數(shù)據(jù)椰憋。

PPWebFetchCache對(duì)外暴露的接口只有存和取厅克,而具體的加入和刪除操作則是在內(nèi)部通過(guò)maxMemoryCost、maxMemoryCountLimit控制實(shí)現(xiàn)橙依。

異步環(huán)境

顯然在數(shù)據(jù)存取的時(shí)候证舟,我們不應(yīng)該放在主線程中去做這些事兒。因此我創(chuàng)建串行隊(duì)列用來(lái)執(zhí)行這些事務(wù):

static dispatch_queue_t background_serialQueue(){
    static dispatch_queue_t queue;
    if (!queue) {
        char *const label = "com.example.webprefetchCache";
        dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0);
        queue = dispatch_queue_create(label, attr);
    }
    return queue;
}

void async_inbackground(void(^block)(void)){
    dispatch_async(background_serialQueue(), block);
}

為什么要用串行隊(duì)列票编?因?yàn)樵陉?duì)列內(nèi)部操作褪储,我不需要關(guān)心會(huì)出現(xiàn)資源競(jìng)爭(zhēng)的問(wèn)題。而在串行隊(duì)列以外其他隊(duì)列中來(lái)操作單例的相關(guān)數(shù)據(jù)時(shí)慧域,我就需要去關(guān)心的線程安全的問(wèn)題鲤竹。因此我直接使用適用于靜態(tài)分配的的互斥量PTHREAD_MUTEX_INITIALIZER來(lái)保證數(shù)據(jù)的同步操作。反觀如果我去使用pthread_mutex_init來(lái)動(dòng)態(tài)生成一個(gè)互斥量的話昔榴,我還要操心什么時(shí)候去destroy掉它(當(dāng)然這里得仔細(xì)思考造成死鎖的問(wèn)題)辛藻。

static inline pthread_mutex_t mutext_lock(void){
    static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
    return lock;
}

static void safeProgressWith(void(^block)(void)){
    pthread_mutex_t lock = mutext_lock();
    pthread_mutex_lock(&lock);
    block();
    pthread_mutex_unlock(&lock);
}

在加鎖等操作時(shí),盡量讓其顆粒度更低互订。這樣可以減少不必要的線程處于waiting狀態(tài)吱肌,也就相應(yīng)地減少出現(xiàn)低優(yōu)先級(jí)線程餓死的情況發(fā)生(盡量減少CPU密集型線程的時(shí)間片)。

LFU的具體實(shí)現(xiàn)

LFU只是針對(duì)于URL的淘汰策略仰禽,淘汰了URL之后氮墨,根據(jù)該URL到NSDictionary中找出對(duì)應(yīng)的數(shù)據(jù)進(jìn)行移除。這里使用鏈表的方式實(shí)現(xiàn)棧結(jié)構(gòu)吐葵,其結(jié)構(gòu)如下:

typedef struct __PPWebLFUFetchInlayerNode_ * _PPWebLFUInlayerNodeRef;
typedef struct __PPWebLFUFetchInlayerNode_ {
    char *url;
    int use_count;
    _PPWebLFUInlayerNodeRef next;
}_PPWebLFUFetchInlayerNode;

在PPWebFetchLFUCache類(lèi)中保存了一個(gè)_PPWebLFUInlayerNodeRef的指針规揪,這個(gè)指針指向棧頂:

@interface PPWebFetchLFUCache:PPWebFetchCache{
    _PPWebLFUInlayerNodeRef _lfuStack;
}

走到這里我們大可以直接使用_lfuStack成員變量來(lái)對(duì)棧進(jìn)行相應(yīng)的操作,但是我們可以更進(jìn)一步温峭!這里思維跳躍一下猛铅,當(dāng)我們?cè)诓迦胍粋€(gè)節(jié)點(diǎn)時(shí)如何去判斷當(dāng)前節(jié)點(diǎn)是新增節(jié)點(diǎn)、還是存在于棧中的節(jié)點(diǎn)凤藏、抑或是需要?jiǎng)h除的結(jié)點(diǎn)奸忽?
如果只是簡(jiǎn)單的回答“我在插入節(jié)點(diǎn)時(shí),先遍歷一遍椧咀看元素是否存在于其中”栗菜,這樣做毫無(wú)意義,而且平添一倍的時(shí)間消耗蹄梢,因?yàn)楹竺娴牟迦氩僮鲿r(shí)疙筹,還要去遍歷一次找到對(duì)應(yīng)的節(jié)點(diǎn)位置。
為了能夠更好地在同一個(gè)循環(huán)中處理插入數(shù)據(jù),查找數(shù)據(jù)腌歉,刪除數(shù)據(jù)等操作。這里需要在執(zhí)行C操作時(shí)在適當(dāng)?shù)攸c(diǎn)給我們回調(diào)齐苛,讓我們有機(jī)會(huì)在一次循環(huán)中做完這些操作翘盖。為什么要用回調(diào)呢?我們完全可以把刪除的相關(guān)邏輯放在某一次循環(huán)中凹蜂,這樣就需要我們?cè)谡{(diào)用邏輯時(shí)傳入一些判斷條件馍驯。這無(wú)疑是增加了算法的局限性,從另一點(diǎn)來(lái)說(shuō)玛痊,這個(gè)算法的適用范圍就大大降低了汰瘫。
所以我引入了一個(gè)上下文環(huán)境,這個(gè)環(huán)境主要用于包裹相關(guān)信息數(shù)據(jù)和函數(shù)指針回調(diào):

typedef struct __PPWebLFUFetchCacheContext *PPWebLFUFetchCacheContextRef;
typedef struct __PPWebLFUFetchCacheContext {
    _PPWebLFUInlayerNodeRef *node;/// root
    void *info;/// 一般傳入fetchCacher對(duì)象
    void (*appendInStackRootCallback)(void *info, char *const key);/// 棧頂插入回調(diào)
    void (*appendInStackBottomCallback)(void *info, char *const key);/// 棧底插入回調(diào)
    void (*appendInStackCallback)(void *info, char *const key);/// 棧中插入回調(diào)
}PPWebLFUFetchCacheContext;

/// 調(diào)用這個(gè)方法之后擂煞,如果不再需要使用這個(gè)指針混弥,需要調(diào)用free來(lái)釋放內(nèi)存空間
PPWebLFUFetchCacheContextRef PPWebLFUFetchCacheContextCreate(void *root,
                                                           void *info,
                                                           void (* _Nonnull appendInStackRootCallback)(void *info, char *const key),
                                                           void (* _Nonnull appendInStackBottomCallback)(void *info, char *const key),
                                                           void (* _Nonnull appendInStackCallback)(void *info, char *const key)){
    PPWebLFUFetchCacheContextRef ctx = (PPWebLFUFetchCacheContextRef)malloc(sizeof(PPWebLFUFetchCacheContext));
    ctx->node = root;
    ctx->info = info;
    ctx->appendInStackRootCallback = appendInStackRootCallback;
    ctx->appendInStackBottomCallback = appendInStackBottomCallback;
    ctx->appendInStackCallback = appendInStackCallback;
    return ctx;
}

至于這里為什么選擇一個(gè)上下文?當(dāng)我們需要多個(gè)回調(diào)時(shí)对省,完全沒(méi)有必要把每一個(gè)回調(diào)都添加到函數(shù)參數(shù)中去蝗拿,我們可以把這些參數(shù)包裝起來(lái)。而且這樣包裝起來(lái)做還有一個(gè)優(yōu)勢(shì)蒿涎,就是新增回調(diào)場(chǎng)景時(shí)就要方便許多哀托!

元素添加

現(xiàn)在所有的條件都已具備,是時(shí)候來(lái)處理這些具體的邏輯了劳秋。就像是在學(xué)紅黑樹(shù)的時(shí)候一般都會(huì)把它那5個(gè)特性先提出來(lái)是吧仓手。所以這里需要明確幾點(diǎn)特性:

  • 1、由鏈表實(shí)現(xiàn)的一個(gè)棧玻淑,只有一個(gè)根節(jié)點(diǎn)(上面提到的嗽冒,包裝在上下文中的lfustack);
  • 2岁忘、棧的深度是有限制的辛慰;
  • 3、添加和刪除操作是基于棧頂干像;
  • 4帅腌、棧內(nèi)元素的使用次數(shù)是從小到大,從上到下生長(zhǎng)麻汰;

基于以上速客,我定義了一個(gè)元素添加函數(shù)的原型:

bool _PPWebFetchLFUInlayerQueueAdd(PPWebLFUFetchCacheContextRef context,char *const url)

很明顯傳入的context是需要在外面創(chuàng)建好的一個(gè)指針變量,但是context的具體成員變量我們沒(méi)有控制五鲫,全部傳入NULL都可以(因?yàn)閼心缰埃幌雽?duì)函數(shù)指針做非空判斷,所以我把函數(shù)指針設(shè)置為_(kāi)Nonnull。浪耘。乱灵。),這沒(méi)有什么問(wèn)題七冲。因此首先要做的就是判斷棧是否為空:

if (!(*(context->node))) {/// 創(chuàng)建棧頂指針
  _PPWebLFUInlayerNodeRef node = allocNewNode(url);
  if (!node) {
    return false;
  }
  __block _PPWebLFUInlayerNodeRef *_broot = (context->node);
  safeProgressWith(^{
    *_broot = node;
    (context->appendInStackRootCallback)(context->info,url);
  });
  return true;
}

在上面這段代碼中我們使用了context的一個(gè)函數(shù)指針回調(diào)痛倚,當(dāng)在空棧中加入根節(jié)點(diǎn),這是符合該函數(shù)指針回調(diào)語(yǔ)義的澜躺。
此時(shí)的棧分布情況很簡(jiǎn)單蝉稳,但還是畫(huà)出來(lái)更加明顯:



在這之后插入結(jié)點(diǎn)時(shí),我們便需要考慮新添加進(jìn)來(lái)的URL是新節(jié)點(diǎn)還是在原有節(jié)點(diǎn)上增加使用次數(shù)掘鄙。這里我們需要一個(gè)循環(huán)從根節(jié)點(diǎn)開(kāi)始遍歷棧耘戚,如果找到了對(duì)應(yīng)的URL,便將其使用次數(shù)加一操漠,如果走到棧底還是未能命中對(duì)應(yīng)的URL收津,則需要以該URL為數(shù)據(jù)創(chuàng)建一個(gè)新節(jié)點(diǎn),并將這個(gè)節(jié)點(diǎn)作為棧根颅夺。實(shí)現(xiàn)代碼如下:

if (0 == strcmp(lead->url, url)) {
  (context->node)->use_count++;
}else{
  (context->appendInStackRootCallback)(context->info,url);
  return appendRootNodeInStack((context->node), url);
}

上面涉及到的函數(shù)appendRootNodeInStack主要用于生成一個(gè)節(jié)點(diǎn)之后朋截,并將該節(jié)點(diǎn)設(shè)置為根:

bool appendRootNodeInStack(_PPWebLFUInlayerNodeRef *root ,char *const url){
    /// 在棧頂插入
    _PPWebLFUInlayerNodeRef node = allocNewNode(url);
    if (!node) {
        return false;
    }
    __block _PPWebLFUInlayerNodeRef *_broot = root;
    safeProgressWith(^{
        node->next = *_broot;
        *_broot = node;
    });
    return true;
}

所以現(xiàn)在會(huì)出現(xiàn)兩種情況,如下所示:



這段邏輯代碼目前并沒(méi)有放在循環(huán)中來(lái)做吧黄,它和棧中已經(jīng)存在四個(gè)部服、五個(gè)節(jié)點(diǎn)的情況是類(lèi)似的,但它的情況要簡(jiǎn)單許多拗慨,它只需要處理使用次數(shù)更新或者頭節(jié)點(diǎn)插入的情況廓八,不會(huì)涉及到刪除(除非你不做緩存)、移位操作赵抢。到最后我會(huì)把這段代碼合并起來(lái)剧蹂,而那正是我設(shè)計(jì)這套算法的中心思想。

節(jié)點(diǎn)移動(dòng)和刪除

針對(duì)節(jié)點(diǎn)的移動(dòng)烦却,需要考慮多個(gè)情況宠叼,包括:從棧頂移動(dòng)到棧底、從棧頂移動(dòng)到棧中某一個(gè)位置其爵、從棧中某一個(gè)位置移動(dòng)到棧中另一個(gè)位置冒冬、從棧中某一個(gè)位置移動(dòng)到棧底、不移動(dòng)摩渺。
我把這些情況依次描述到圖中简烤,這樣看著更直觀:


前兩種情況

下面是后面兩種情況:


后兩種情況

從上面虛線箭頭到實(shí)線箭頭的變化可以很明顯看出來(lái),是復(fù)雜了不少摇幻。而最后一種無(wú)變化的横侦,我就沒(méi)有列出來(lái)挥萌。看看前面四種情況變化后的棧元素排列情況枉侧,從左到右引瀑,從上到下依次是:
  • 2->3->4->1;
  • 3->2->4->1;
  • 2->4->3->1;
  • 2->3->1->4;

從上面四種情況來(lái)看,對(duì)于一個(gè)節(jié)點(diǎn)的移動(dòng)可以分為兩部拆開(kāi)來(lái)看榨馁,分別是——取出伤疙、放入兩個(gè)過(guò)程。我直接把中間的的算法列出來(lái):

    _PPWebLFUInlayerNodeRef previous = lead;
    _PPWebLFUInlayerNodeRef pivot = NULL;
    _PPWebLFUInlayerNodeRef prepivot = NULL;
    do {
        if (0 != strcmp(lead->url, url)) {
            if (!pivot) {
                continue;
            }
            if (pivot->use_count <= lead->use_count) {
                break;/// 跳出循環(huán)去執(zhí)行放入
            }
            if (*(context->node)==pivot) {
                __block _PPWebLFUInlayerNodeRef *_broot = (context->node);
                safeProgressWith(^{
                    *_broot = previous->next;
                });
            }else{/// 取出
                prepivot->next=pivot->next;
            }
            continue;
        }
        lead->use_count++;
        pivot = lead;
        prepivot = previous;
    } while ((void)(previous=lead),(void)(lead=lead->next),lead);
    if (!pivot) {/// 在棧頂插入
        (context->appendInStackRootCallback)(context->info,url);
        return appendRootNodeInStack((context->node), url);
    }
    if (!lead) {/// 處理?xiàng)5浊闆r
        previous->next=pivot;
        pivot->next=NULL;
        (context->appendInStackBottomCallback)(context->info,url);
    }else{/// 處理?xiàng)V械姆湃?        pivot->next=previous->next;
        previous->next=previous==pivot?lead:pivot;
        (context->appendInStackCallback)(context->info,url);
    }

這段代碼把上面提到的if-else判斷也一起合并進(jìn)來(lái)了辆影,這里pivot主要是用來(lái)記錄找到目標(biāo)URL的哨兵,而prepivot用來(lái)記錄哨兵前面一個(gè)節(jié)點(diǎn)(如果使用雙向鏈表完全可以不用這個(gè)零時(shí)變量)黍特。
到這里基本上是把該算法的核心部分說(shuō)完了蛙讥,該算法的最壞時(shí)間復(fù)雜度就是O(N),這種最壞時(shí)間復(fù)雜度的情況分別是:新節(jié)點(diǎn)插入灭衷,棧頂一次直接移動(dòng)棧底(這個(gè)情況是發(fā)生在使用次數(shù)都為1時(shí)次慢,棧頂元素此時(shí)+1的情況)。最優(yōu)的時(shí)間復(fù)雜度情況是O(1)翔曲,直接棧頂數(shù)據(jù)更新迫像。
最后就是節(jié)點(diǎn)的刪除操作,僅僅只是刪除操作時(shí)間復(fù)雜度肯定是O(1)的瞳遍。但是事情往往沒(méi)有這么簡(jiǎn)單闻妓,必須要考慮當(dāng)前添加進(jìn)來(lái)的元素是到達(dá)容量限制的新元素,還是棧里面已經(jīng)存在的元素呢掠械?難道我們又要去遍歷一次棧然后來(lái)做刪除操作嗎由缆?這是完全沒(méi)有必要的,因?yàn)橐霈F(xiàn)刪除節(jié)點(diǎn)的情況猾蒂,肯定是發(fā)生在向棧中Push元素時(shí)發(fā)生均唉。
因此我將上面各個(gè)情況分為三種大體情況,并為這三種情況提供了三個(gè)回調(diào)肚菠,而這個(gè)三個(gè)回調(diào)都是放在上面的context中:

  • 在棧頂插入元素(appendInStackRootCallback)舔箭;
  • 處理?xiàng)5浊闆r(appendInStackBottomCallback);
  • 處理中間節(jié)點(diǎn)(appendInStackCallback)蚊逢;

基于這:

我們可以一次循環(huán)中完成新增层扶、移動(dòng)、刪除操作时捌!

上面提到的三個(gè)回調(diào)怒医,可以通過(guò)調(diào)用PPWebFetchLFUCache實(shí)例方法來(lái)看一下一個(gè)整體過(guò)程:

- (BOOL)insertCacheMap:(NSData *)object forKey:(NSString *)key{
    PPWebLFUFetchCacheContextRef ctx = PPWebLFUFetchCacheContextCreate(
                                                                       &_lfuStack,
                                                                       (__bridge void *)(self),
                                                                       &progressingAppendInStackRootCallback,
                                                                       &progressingAppendInStackBottomCallback,
                                                                       &progressingAppendInStackCallback);
    bool result = _PPWebFetchLFUInlayerQueueAdd(ctx, (char *)[key UTF8String]);
    if (result == false) {
        PPWebLFUFetchCacheContextRelease(&ctx);
        return NO;
    }
    if (![self.cacheMap.allKeys containsObject:key]) {
        safeProgressWith(^{
            [self.cacheMap setObject:object forKey:key];
            self.currentCacheUsage += object.length;
        });
    }
    PPWebLFUFetchCacheContextRelease(&ctx);
    return YES;
}

在上面創(chuàng)建上下文的代碼中,第一個(gè)參數(shù)為保存在PPWebFetchLFUCache單例中的一個(gè)成員變量奢讨,而info參數(shù)主要用來(lái)傳遞self稚叹,這里用context時(shí)焰薄,_lfuStack會(huì)被context保留,而_lfuStack又會(huì)被PPWebFetchLFUCache單例保留扒袖,但是在函數(shù)返回之前會(huì)對(duì)context做release操作塞茅,會(huì)把對(duì)_lfuStack的保留置空,所以不要想著OC里面常出現(xiàn)的引用計(jì)數(shù)不會(huì)降為0的問(wèn)題季率。而且也不會(huì)出現(xiàn)相互持有的關(guān)系野瘦。
而回調(diào)函數(shù)中,主要來(lái)看progressingAppendInStackRootCallback的回調(diào):

void progressingAppendInStackRootCallback(void *info, char *const key){
    PPWebFetchLFUCache *cacher = (__bridge PPWebFetchLFUCache *)(info);
    if (!cacher) {
        return;
    }
    if (cacher.cacheMap.allKeys.count >= kMaxMemoryCountLimit) {
        [cacher deleteCacheMap];
        progressingAppendInStackRootCallback(info, key);
    }else{
        return;
    }
}

下面我直接把刪除函數(shù)貼出來(lái)飒泻,這并沒(méi)有什么難點(diǎn):

_PPWebLFUInlayerNodeRef _PPWebFetchLFUInlayerQueueDelete(PPWebLFUFetchCacheContextRef context ,char **url){
    if (!(context->node)) {
        return NULL;
    }
    _PPWebLFUInlayerNodeRef lead = NULL;
    lead = *(context->node);
    __block _PPWebLFUInlayerNodeRef *_broot = (context->node);
    if ((*_broot)->next) {
        safeProgressWith(^{
            *_broot = (*_broot)->next;
        });
    }
    if (url) {
         (*url) = (lead->url);
    }
    return lead;
}

上面出現(xiàn)的deleteCacheMap方法中會(huì)把不再使用的節(jié)點(diǎn)free掉鞭光。

后語(yǔ)

上面代碼中很多地方都用到了safeProgressWith函數(shù),其實(shí)現(xiàn)也在上面列出來(lái)了泞遗。使用它的目的有兩個(gè):
第一個(gè)惰许、PPWebFetchLFUCache類(lèi)的操作是可以在多線程異步環(huán)境下操作的,所以我必須要保證cacheMap的數(shù)據(jù)同步史辙;
第二個(gè)汹买、雖然基于C的操作我都是放在一個(gè)串行的隊(duì)列中進(jìn)行:

async_inbackground(^{
  BOOL result = [self insertCacheMap:data forKey:url];
  if (complete) {
    complete(result);
  }
});

但是_lfuStack成員變量是可以通過(guò)hook的方法拿到,并讓其在異步環(huán)境下去進(jìn)行修改聊倔,這個(gè)我沒(méi)法去控制晦毙,但是我要做到在LFU內(nèi)做到一個(gè)同步操作,所以基于跟節(jié)點(diǎn)的操作都是在加鎖狀態(tài)下完成的耙蔑。這里需要注意的就是不要出現(xiàn)互斥鎖的嵌套使用见妒,如果使用的是同一個(gè)鎖變量的話,那肯定會(huì)造成死鎖的甸陌。

完徐鹤。。邀层。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末返敬,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子寥院,更是在濱河造成了極大的恐慌劲赠,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,692評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件秸谢,死亡現(xiàn)場(chǎng)離奇詭異凛澎,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)估蹄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)塑煎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人臭蚁,你說(shuō)我怎么就攤上這事最铁⊙渡停” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,995評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵冷尉,是天一觀的道長(zhǎng)漱挎。 經(jīng)常有香客問(wèn)我,道長(zhǎng)雀哨,這世上最難降的妖魔是什么磕谅? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,223評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮雾棺,結(jié)果婚禮上膊夹,老公的妹妹穿的比我還像新娘。我一直安慰自己捌浩,他們只是感情好割疾,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著嘉栓,像睡著了一般。 火紅的嫁衣襯著肌膚如雪拓诸。 梳的紋絲不亂的頭發(fā)上侵佃,一...
    開(kāi)封第一講書(shū)人閱讀 51,208評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音奠支,去河邊找鬼馋辈。 笑死,一個(gè)胖子當(dāng)著我的面吹牛倍谜,可吹牛的內(nèi)容都是我干的迈螟。 我是一名探鬼主播,決...
    沈念sama閱讀 40,091評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼尔崔,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼答毫!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起季春,我...
    開(kāi)封第一講書(shū)人閱讀 38,929評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤洗搂,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后载弄,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體耘拇,經(jīng)...
    沈念sama閱讀 45,346評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評(píng)論 2 333
  • 正文 我和宋清朗相戀三年宇攻,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了惫叛。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,739評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡逞刷,死狀恐怖嘉涌,靈堂內(nèi)的尸體忽然破棺而出妻熊,到底是詐尸還是另有隱情,我是刑警寧澤洛心,帶...
    沈念sama閱讀 35,437評(píng)論 5 344
  • 正文 年R本政府宣布固耘,位于F島的核電站,受9級(jí)特大地震影響词身,放射性物質(zhì)發(fā)生泄漏厅目。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評(píng)論 3 326
  • 文/蒙蒙 一法严、第九天 我趴在偏房一處隱蔽的房頂上張望损敷。 院中可真熱鬧,春花似錦深啤、人聲如沸拗馒。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,677評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)诱桂。三九已至,卻和暖如春呈昔,著一層夾襖步出監(jiān)牢的瞬間挥等,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,833評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工堤尾, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留肝劲,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,760評(píng)論 2 369
  • 正文 我出身青樓郭宝,卻偏偏與公主長(zhǎng)得像辞槐,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子粘室,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理榄檬,服務(wù)發(fā)現(xiàn),斷路器衔统,智...
    卡卡羅2017閱讀 134,652評(píng)論 18 139
  • 點(diǎn)擊查看原文 Web SDK 開(kāi)發(fā)手冊(cè) SDK 概述 網(wǎng)易云信 SDK 為 Web 應(yīng)用提供一個(gè)完善的 IM 系統(tǒng)...
    layjoy閱讀 13,758評(píng)論 0 15
  • 1 序 2016年6月25日夜丙号,帝都,天下著大雨缰冤,拖著行李箱和同學(xué)在校門(mén)口照了最后一張合照犬缨,搬離寢室打車(chē)去了提前租...
    RichardJieChen閱讀 5,096評(píng)論 0 12
  • 三八了怀薛,白天要上課,老媽要上班也沒(méi)好打擾迷郑。這不枝恋,等到晚上下課做完事后创倔,我打開(kāi)微信,發(fā)了長(zhǎng)這么大以來(lái)第一條祝福老媽節(jié)...
    鄧義福閱讀 849評(píng)論 0 5
  • 《十字繡》 夢(mèng)想在夜晚穿行焚碌,可愛(ài)的姑娘 體內(nèi)亮起星星的光芒畦攘! 露水,穿針引線十电。河流知押,兜兜轉(zhuǎn)轉(zhuǎn) 你愛(ài)的姑娘比山花爛漫...
    幽明m閱讀 222評(píng)論 6 4