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

1 LRU算法

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

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

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

2 使用NSDictionary實現

2.1 實現代碼

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

頭文件如下,支持泛型:

#import <Foundation/Foundation.h>

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

// maxCountLRU: 執行LRU算法時的最大存儲的元素數量
- (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
// 執行LRU算法,當訪問的元素可能是被淘汰的時候,可以通過在block中返回需要訪問的對象,會根據LRU機制自動添加到dictionary中
- (ObjectType)objectForKey:(KeyType)aKey returnEliminateObjectUsingBlock:(ObjectType (^)(BOOL maybeEliminate))block;

@end

實現文件如下:

#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];
        
        // 【注意】這里不要直接調用下面這個方法,因為內部調用[_arrayForLRU removeObject:anObject];的時候,
        // 每次都將Array從頭開始遍歷到最后一個,這里既然已經知道是刪除最后一個了,直接刪除即可。
        // 使用下面這種方法會增加上百倍的耗時。
        // [self removeObjectForKey:[_arrayForLRU lastObject]];
    }
}

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

@end

2.2 實現原理概述

  • 實現原理其實很簡單,重寫NSMutableDictionary的幾個重要方法,內部持有NSMutableDictionary用于存儲緩存數據。持有NSMutableArray用于存儲Key值,Key的順序為LRU算法中的優先級,最前面的元素表示最近使用,最后面的元素表示最近最少使用。
  • 每次對NSMutableDictionary中的元素做數據操作,都認為這個元素是最近使用的元素,然后調整該元素的KeyNSMutableArray中的順序為第一位。
  • 設定一個容器可存儲的數量最大值,當插入元素到超出這個最大值之后,在NSMutableArray中找到最后一個元素Key刪除,并在NSMutableDictionary中找到對應的元素刪除。

2.3 弊端

  1. 以上的實現在對性能要求不是特別高的時候,已經可以滿足需求了。
  2. 我們知道,NSMutableArray的內部是使用動態數組實現的,動態數組的缺點在這里被完全暴露出來,我們的實現里面基本上都在用到“插入”和“刪除”的操作,這兩個操作對動態數組的性能消耗是比較大的,比較好的實現方式應該是使用“雙向鏈表”,奈何Foundation框架中沒有我們想要的“雙向鏈表”容器。當然我們可以使用C++的STL庫中的list來實現,有興趣的讀者可以嘗試。
  3. 容器中有設定一個容器的最大存儲容量值,我相信,在使用這個LRU容器時,絕大部分的情況下都是達不到這個最大容量的,淘汰算法應該是屬于一種保護措施不是嗎?那么問題來了,如果大部分情況下都達不到最大容量,或者可能永遠都達不到最大容量,那我們每次對元素操作時做了LRU算法調整,不是白費功夫嗎(這個調整還是要消耗一些性能的),畢竟這個調整最終是為了當超出容量時用來將最后面的元素淘汰而做的準備,想想是不是可以以此做一些優化?

3 性能優化

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

頭文件不變,來看實現文件:

#import "LRUMutableDictionary.h"

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

@interface LRUMutableDictionary ()

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

@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,現在增加一個元素之后達到了存儲量臨界條件,則開啟,一次性將所有的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];
            
            // 【注意】這里不要直接調用下面這個方法,因為內部調用[_arrayForLRU removeObject:anObject];的時候,
            // 每次都將Array從頭開始遍歷到最后一個,這里既然已經知道是刪除最后一個了,直接刪除即可。
            // 使用下面這種方法會增加上百倍的耗時。
            // [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 接近值可以根據實際情況做一些修改,以上實現方法已經在項目中實際使用,可以很好的滿足需求。

讀者如果對于實現LRU算法有更好的方法,歡迎討論。

本文首發于我的博客vinnyxiong.cn,歡迎訪問。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容