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算法的容器。

头文件如下,支持泛型:

LRUMutableDictionary.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#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

实现文件如下:

LRUMutableDictionary.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#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算法。

头文件不变,来看实现文件:

LRUMutableDictionary.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
#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算法有更好的方法,欢迎讨论。

您的支持将鼓励我继续创作!
0%