iOS-OC實現(xiàn)LRU算法NSDictionary容器(非線程安全)

1 LRU算法

LRU(Least recently used余蟹,最近最少使用)算法根據(jù)數(shù)據(jù)的歷史訪問記錄來進行淘汰數(shù)據(jù)梅掠,其核心思想是“如果數(shù)據(jù)最近被訪問過刚盈,那么將來被訪問的幾率也更高”胎许。

這篇文章對LRU緩存算法做了非常詳細的介紹:緩存淘汰算法之LRU-OYK

可惜Foundation框架中并未提供一個比較簡潔的LRU算法找前,NSCache沒怎么看懂,java中有LruCache调俘。

2 使用NSDictionary實現(xiàn)

2.1 實現(xiàn)代碼

為了便于查找伶棒,緩存通常都是Dictionary的形式旺垒,這里也是通過繼承NSMutableDictionary來實現(xiàn)一個包含LRU算法的容器。

頭文件如下肤无,支持泛型:

#import <Foundation/Foundation.h>

@interface LRUMutableDictionary<__covariant KeyType, __covariant ObjectType> : NSObject

// maxCountLRU: 執(zhí)行LRU算法時的最大存儲的元素數(shù)量
- (instancetype)initWithMaxCountLRU:(NSUInteger)maxCountLRU;

//*****NSDictionary
@property (readonly) NSUInteger count;

- (NSEnumerator<KeyType> *)keyEnumerator;

- (void)enumerateKeysAndObjectsUsingBlock:(void (^)(KeyType key, ObjectType obj, BOOL *stop))block;

//*****NSMutableDictionary
- (void)removeObjectForKey:(KeyType)aKey;
- (void)setObject:(ObjectType)anObject forKey:(KeyType <NSCopying>)aKey;

- (void)removeAllObjects;
- (void)removeObjectsForKeys:(NSArray<KeyType> *)keyArray;

//*****LRUMutableDictionary
// 執(zhí)行LRU算法先蒋,當訪問的元素可能是被淘汰的時候,可以通過在block中返回需要訪問的對象舅锄,會根據(jù)LRU機制自動添加到dictionary中
- (ObjectType)objectForKey:(KeyType)aKey returnEliminateObjectUsingBlock:(ObjectType (^)(BOOL maybeEliminate))block;

@end

實現(xiàn)文件如下:

#import "LRUMutableDictionary.h"

@interface LRUMutableDictionary ()

@property (nonatomic, strong) NSMutableDictionary *dict;
@property (nonatomic, strong) NSMutableArray *arrayForLRU;
@property (nonatomic, assign) NSUInteger maxCountLRU;

@end

@implementation LRUMutableDictionary

- (instancetype)initWithMaxCountLRU:(NSUInteger)maxCountLRU
{
    self = [super init];
    if (self) {
        _dict = [[NSMutableDictionary alloc] initWithCapacity:maxCountLRU];
        _arrayForLRU = [[NSMutableArray alloc] initWithCapacity:maxCountLRU];
        _maxCountLRU = maxCountLRU;
    }
    return self;
}
#pragma mark - NSDictionary

- (NSUInteger)count
{
    return [_dict count];
}

- (NSEnumerator *)keyEnumerator
{
    return [_dict keyEnumerator];
}

- (id)objectForKey:(id)aKey
{
    return [self objectForKey:aKey returnEliminateObjectUsingBlock:^id(BOOL maybeEliminate) {
        return nil;
    }];
}

- (void)enumerateKeysAndObjectsUsingBlock:(void (^)(id, id, BOOL *))block
{
    [_dict enumerateKeysAndObjectsUsingBlock:block];
}

#pragma mark - NSMutableDictionary

- (void)removeObjectForKey:(id)aKey
{
    [_dict removeObjectForKey:aKey];
    [self _removeObjectLRU:aKey];
}

- (void)setObject:(id)anObject forKey:(id<NSCopying>)aKey
{
    BOOL isExist = ([_dict objectForKey:aKey] != nil);
    [_dict setObject:anObject forKey:aKey];
    
    if (isExist) {
        [self _adjustPositionLRU:aKey];
    } else {
        [self _addObjectLRU:aKey];
    }
}

- (void)removeAllObjects
{
    [_dict removeAllObjects];
    [_arrayForLRU removeAllObjects];
}

- (void)removeObjectsForKeys:(NSArray *)keyArray
{
    [_dict removeObjectsForKeys:keyArray];
    [_arrayForLRU removeObjectsInArray:keyArray];
}

#pragma mark - LRUMutableDictionary

- (id)objectForKey:(id)aKey returnEliminateObjectUsingBlock:(id (^)(BOOL))block
{
    id object = [_dict objectForKey:aKey];
    if (object) {
        [self _adjustPositionLRU:aKey];
    }
    if (block) {
        BOOL maybeEliminate = object ? NO : YES;
        id newObject = block(maybeEliminate);
        if (newObject) {
            [self setObject:newObject forKey:aKey];
            return [_dict objectForKey:aKey];
        }
    }
    return object;
}

#pragma mark - LRU

- (void)_adjustPositionLRU:(id)anObject
{
    NSUInteger idx = [_arrayForLRU indexOfObject:anObject];
    if (idx != NSNotFound) {
        [_arrayForLRU removeObjectAtIndex:idx];
        [_arrayForLRU insertObject:anObject atIndex:0];
    }
}

- (void)_addObjectLRU:(id)anObject
{
    [_arrayForLRU insertObject:anObject atIndex:0];
    // 當超出LRU算法限制之后鞭达,將最不常使用的元素淘汰
    if ((_maxCountLRU > 0) && (_arrayForLRU.count > _maxCountLRU)) {
        [_dict removeObjectForKey:[_arrayForLRU lastObject]];
        [_arrayForLRU removeLastObject];
        
        // 【注意】這里不要直接調(diào)用下面這個方法,因為內(nèi)部調(diào)用[_arrayForLRU removeObject:anObject];的時候皇忿,
        // 每次都將Array從頭開始遍歷到最后一個畴蹭,這里既然已經(jīng)知道是刪除最后一個了,直接刪除即可鳍烁。
        // 使用下面這種方法會增加上百倍的耗時叨襟。
        // [self removeObjectForKey:[_arrayForLRU lastObject]];
    }
}

- (void)_removeObjectLRU:(id)anObject
{
    [_arrayForLRU removeObject:anObject];
}

@end

2.2 實現(xiàn)原理概述

  • 實現(xiàn)原理其實很簡單,重寫NSMutableDictionary的幾個重要方法幔荒,內(nèi)部持有NSMutableDictionary用于存儲緩存數(shù)據(jù)糊闽。持有NSMutableArray用于存儲Key值,Key的順序為LRU算法中的優(yōu)先級爹梁,最前面的元素表示最近使用右犹,最后面的元素表示最近最少使用。
  • 每次對NSMutableDictionary中的元素做數(shù)據(jù)操作姚垃,都認為這個元素是最近使用的元素念链,然后調(diào)整該元素的KeyNSMutableArray中的順序為第一位。
  • 設定一個容器可存儲的數(shù)量最大值积糯,當插入元素到超出這個最大值之后掂墓,在NSMutableArray中找到最后一個元素Key刪除,并在NSMutableDictionary中找到對應的元素刪除看成。

2.3 弊端

  1. 以上的實現(xiàn)在對性能要求不是特別高的時候君编,已經(jīng)可以滿足需求了。
  2. 我們知道川慌,NSMutableArray的內(nèi)部是使用動態(tài)數(shù)組實現(xiàn)的吃嘿,動態(tài)數(shù)組的缺點在這里被完全暴露出來,我們的實現(xiàn)里面基本上都在用到“插入”和“刪除”的操作梦重,這兩個操作對動態(tài)數(shù)組的性能消耗是比較大的兑燥,比較好的實現(xiàn)方式應該是使用“雙向鏈表”,奈何Foundation框架中沒有我們想要的“雙向鏈表”容器忍饰。當然我們可以使用C++的STL庫中的list來實現(xiàn)贪嫂,有興趣的讀者可以嘗試寺庄。
  3. 容器中有設定一個容器的最大存儲容量值艾蓝,我相信力崇,在使用這個LRU容器時,絕大部分的情況下都是達不到這個最大容量的赢织,淘汰算法應該是屬于一種保護措施不是嗎亮靴?那么問題來了,如果大部分情況下都達不到最大容量于置,或者可能永遠都達不到最大容量茧吊,那我們每次對元素操作時做了LRU算法調(diào)整,不是白費功夫嗎(這個調(diào)整還是要消耗一些性能的)八毯,畢竟這個調(diào)整最終是為了當超出容量時用來將最后面的元素淘汰而做的準備搓侄,想想是不是可以以此做一些優(yōu)化?

3 性能優(yōu)化

針對第三點弊端做一些優(yōu)化话速,當容量達不到最大容量值得時候讶踪,可以完全停止掉LRU算法,這時候這個LRU容器就跟普通的NSMutableDictionary容器沒什么兩樣了泊交,當容量接近最大容量值得時候乳讥,開始啟動LRU算法。

頭文件不變廓俭,來看實現(xiàn)文件:

#import "LRUMutableDictionary.h"

// 定義一個開始準備啟動LRU的值云石,當 (MaxCount - CurCount < LRU_RISK_COUNT) 時才啟動 LRU
#define LRU_RISK_COUNT 100

@interface LRUMutableDictionary ()

@property (nonatomic, strong) NSMutableDictionary *dict;    // 存儲數(shù)據(jù)的字典
@property (nonatomic, strong) NSMutableArray *arrayForLRU;  // 存放LRU的數(shù)組

@property (nonatomic, assign) NSUInteger maxCountLRU;       // 最大存儲值,存儲量超出這個值研乒,啟動LRU淘汰算法
@property (nonatomic, assign) BOOL isOpenLRU;               // 是否開啟LRU算法汹忠,如果存儲量遠低于最大存儲值時,其實沒有必要開啟LRU算法

@end

@implementation LRUMutableDictionary

- (instancetype)initWithMaxCountLRU:(NSUInteger)maxCountLRU
{
    self = [super init];
    if (self) {
        _dict = [[NSMutableDictionary alloc] initWithCapacity:maxCountLRU];
        _arrayForLRU = [[NSMutableArray alloc] initWithCapacity:maxCountLRU];
        _maxCountLRU = maxCountLRU;
    }
    return self;
}
#pragma mark - NSDictionary

- (NSUInteger)count
{
    return [_dict count];
}

- (NSEnumerator *)keyEnumerator
{
    return [_dict keyEnumerator];
}

- (id)objectForKey:(id)aKey
{
    return [self objectForKey:aKey returnEliminateObjectUsingBlock:^id(BOOL maybeEliminate) {
        return nil;
    }];
}

- (void)enumerateKeysAndObjectsUsingBlock:(void (^)(id, id, BOOL *))block
{
    [_dict enumerateKeysAndObjectsUsingBlock:block];
}

#pragma mark - NSMutableDictionary

- (void)removeObjectForKey:(id)aKey
{
    [_dict removeObjectForKey:aKey];
    [self _removeObjectLRU:aKey];
}

- (void)setObject:(id)anObject forKey:(id<NSCopying>)aKey
{
    BOOL isExist = ([_dict objectForKey:aKey] != nil);
    [_dict setObject:anObject forKey:aKey];
    
    if (isExist) {
        [self _adjustPositionLRU:aKey];
    } else {
        [self _addObjectLRU:aKey];
    }
}

- (void)removeAllObjects
{
    [_dict removeAllObjects];
    [self _removeAllObjectsLRU];
}

- (void)removeObjectsForKeys:(NSArray *)keyArray
{
    if (keyArray.count > 0) {
        [_dict removeObjectsForKeys:keyArray];
        [self _removeObjectsLRU:keyArray];
    }
}

#pragma mark - LRUMutableDictionary

- (id)objectForKey:(id)aKey returnEliminateObjectUsingBlock:(id (^)(BOOL))block
{
    id object = [_dict objectForKey:aKey];
    if (object) {
        [self _adjustPositionLRU:aKey];
    }
    if (block) {
        BOOL maybeEliminate = object ? NO : YES;
        id newObject = block(maybeEliminate);
        if (newObject) {
            [self setObject:newObject forKey:aKey];
            return [_dict objectForKey:aKey];
        }
    }
    return object;
}

#pragma mark - LRU

- (void)_adjustPositionLRU:(id)anObject
{
    if (_isOpenLRU) {
        NSUInteger idx = [_arrayForLRU indexOfObject:anObject];
        if (idx != NSNotFound) {
            [_arrayForLRU removeObjectAtIndex:idx];
            [_arrayForLRU insertObject:anObject atIndex:0];
        }
    }
}

- (void)_addObjectLRU:(id)anObject
{
    if (!_isOpenLRU && [self isNeedOpenLRU:_dict.count]) {
        // 如果原來沒有開啟 LRU告嘲,現(xiàn)在增加一個元素之后達到了存儲量臨界條件错维,則開啟,一次性將所有的Key導入
        [_arrayForLRU removeAllObjects];
        [_arrayForLRU addObjectsFromArray:_dict.allKeys];
        [_arrayForLRU removeObject:anObject];
        _isOpenLRU = YES;
    }
    
    if (_isOpenLRU) {
        [_arrayForLRU insertObject:anObject atIndex:0];
        // 當超出LRU算法限制之后橄唬,將最不常使用的元素淘汰
        if ((_maxCountLRU > 0) && (_arrayForLRU.count > _maxCountLRU)) {
            [_dict removeObjectForKey:[_arrayForLRU lastObject]];
            [_arrayForLRU removeLastObject];
            
            // 【注意】這里不要直接調(diào)用下面這個方法赋焕,因為內(nèi)部調(diào)用[_arrayForLRU removeObject:anObject];的時候,
            // 每次都將Array從頭開始遍歷到最后一個仰楚,這里既然已經(jīng)知道是刪除最后一個了隆判,直接刪除即可。
            // 使用下面這種方法會增加上百倍的耗時僧界。
            // [self removeObjectForKey:[_arrayForLRU lastObject]];
        }
    }
}

- (void)_removeObjectLRU:(id)anObject
{
    if (_isOpenLRU) {
        [_arrayForLRU removeObject:anObject];
        
        if (![self isNeedOpenLRU:_arrayForLRU.count]) {
            [_arrayForLRU removeAllObjects];
            _isOpenLRU = NO;
        }
    }
}

- (void)_removeObjectsLRU:(NSArray *)otherArray
{
    if (_isOpenLRU) {
        [_arrayForLRU removeObjectsInArray:otherArray];
        
        if (![self isNeedOpenLRU:_arrayForLRU.count]) {
            [_arrayForLRU removeAllObjects];
            _isOpenLRU = NO;
        }
    }
}

- (void)_removeAllObjectsLRU
{
    if (_isOpenLRU) {
        [_arrayForLRU removeAllObjects];
        _isOpenLRU = NO;    // 清空全部元素了侨嘀,一定可以關閉LRU
    }
}

- (BOOL)isNeedOpenLRU:(NSUInteger)count
{
    return (_maxCountLRU - count) < LRU_RISK_COUNT;
}

@end

上面定義 100 接近值可以根據(jù)實際情況做一些修改,以上實現(xiàn)方法已經(jīng)在項目中實際使用捂襟,可以很好的滿足需求咬腕。

讀者如果對于實現(xiàn)LRU算法有更好的方法,歡迎討論葬荷。

本文首發(fā)于我的博客vinnyxiong.cn涨共,歡迎訪問纽帖。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市举反,隨后出現(xiàn)的幾起案子懊直,更是在濱河造成了極大的恐慌,老刑警劉巖火鼻,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件室囊,死亡現(xiàn)場離奇詭異,居然都是意外死亡魁索,警方通過查閱死者的電腦和手機融撞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來粗蔚,“玉大人懦铺,你說我怎么就攤上這事≈ЪΓ” “怎么了冬念?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長牧挣。 經(jīng)常有香客問我急前,道長,這世上最難降的妖魔是什么瀑构? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任裆针,我火速辦了婚禮,結(jié)果婚禮上寺晌,老公的妹妹穿的比我還像新娘世吨。我一直安慰自己,他們只是感情好呻征,可當我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布耘婚。 她就那樣靜靜地躺著,像睡著了一般陆赋。 火紅的嫁衣襯著肌膚如雪沐祷。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天攒岛,我揣著相機與錄音赖临,去河邊找鬼。 笑死灾锯,一個胖子當著我的面吹牛兢榨,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼吵聪,長吁一口氣:“原來是場噩夢啊……” “哼誊册!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起暖璧,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎君旦,沒想到半個月后澎办,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡金砍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年局蚀,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片恕稠。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡琅绅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出鹅巍,到底是詐尸還是另有隱情千扶,我是刑警寧澤,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布骆捧,位于F島的核電站澎羞,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏敛苇。R本人自食惡果不足惜妆绞,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望枫攀。 院中可真熱鬧括饶,春花似錦、人聲如沸来涨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蹦掐。三九已至楞泼,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間笤闯,已是汗流浹背堕阔。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留颗味,地道東北人超陆。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親时呀。 傳聞我的和親對象是個殘疾皇子张漂,可洞房花燭夜當晚...
    茶點故事閱讀 42,802評論 2 345

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

  • 概述 上一篇主要講解了YYCache的文件結(jié)構(gòu),分析了YYCache類的相關方法谨娜,本章主要分析內(nèi)存緩存類YYMem...
    egoCogito_panf閱讀 3,142評論 2 12
  • 理論總結(jié) 它要解決什么樣的問題航攒? 數(shù)據(jù)的訪問、存取趴梢、計算太慢漠畜、太不穩(wěn)定、太消耗資源坞靶,同時憔狞,這樣的操作存在重復性。因...
    jiangmo閱讀 2,838評論 0 11
  • 1. LRU 1.1.原理 LRU(Leastrecentlyused彰阴,最近最少使用)算法根據(jù)數(shù)據(jù)的歷史訪問記錄來...
    安易學車閱讀 2,520評論 0 23
  • 1我們都聽過 cache尿这,當你問他們是什么是緩存的時候簇抵,他們會給你一個完美的答案,可是他們不知道緩存是怎么構(gòu)建的射众,...
    織田信長閱讀 1,492評論 1 20
  • 當我們在沖動的社會中正压,開始意識到一味追求效率和以自我為中心會摧毀我們的生活。我們便產(chǎn)生絕望和憤怒责球。在金融危機發(fā)生之...
    小播讀書閱讀 627評論 4 7