LRU
LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法;在我们iOS的内存管理里也广泛使用,一些三方库以及系统的NSCache等都有采用这种算法来淘汰那些古老的很久没有被使用的数据
我们要实现这个规则,那么思路就是能够将数据按照使用的顺序来排列起来,当需要淘汰数据的时候就去淘汰这个排列的后面的数据[这里假设我们将最近使用的数据排在前面,你也可以排在后面]
使用数组数据结构实现
- 插入数据,将数据记录在数组的头部(数据已存在就移动到头部)
- 读取数据,将数据移动到数组的头部
- 删除数据,将数据从数组和数据源中移除
- 数据超过阀值,从数组的尾部逐一删除数据同时数据源删除数据
- 数组排序+字典存数据
实现代码
这里使用设置个数的方式来淘汰数据,常用的可以用设置大小或者二者都有的方式
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface LRUByArray : NSObject
@property (nonatomic, assign) NSInteger limitCount;
@property (nonatomic, readonly, strong) NSArray *allItems;
- (void)addObject:(id)obj forKey:(NSString *)key;
- (void)removeObjectByKey:(NSString *)key;
- (id)queryObjectByKey:(NSString *)key;
@end
NS_ASSUME_NONNULL_END
#import "LRUByArray.h"
@interface LRUByArray ()
@property (nonatomic, strong) NSLock *lock;
@property (nonatomic, strong) NSMutableArray *objects; // 用来排序
@property (nonatomic, strong) NSMutableDictionary *map; // 用来存储数据
@end
@implementation LRUByArray
- (instancetype)init {
self = [super init];
if (self) {
_limitCount = 4;
_lock = [NSLock new];
}
return self;
}
- (void)addObject:(id)obj forKey:(nonnull NSString *)key {
if (key == nil) {
return;
}
if (obj == nil) {
[self.lock lock];
[self.objects removeObject:key];
[self.map removeObjectForKey:key];
[self.lock unlock];
return;
}
[self.lock lock];
// 插入数据
id tmpObj = [self.map objectForKey:key];
if (tmpObj) {
[self.objects removeObject:key];
}
[self.map setObject:obj forKey:key];
[self.objects insertObject:key atIndex:0];
// 判断是否超过限制
if (self.map.count > self.limitCount) {
// remove
while (self.objects.count > self.limitCount) {
id lastKey = self.objects.lastObject;
[self.objects removeLastObject];
[self.map removeObjectForKey:lastKey];
}
}
[self.lock unlock];
}
- (void)removeObjectByKey:(NSString *)key {
[self.lock lock];
id obj = [self.map objectForKey:key];
if (obj) {
[self.map removeObjectForKey:key];
[self.objects removeObject:key];
}
[self.lock unlock];
}
- (id)queryObjectByKey:(NSString *)key {
[self.lock lock];
id obj = [self.map objectForKey:key];
if (obj) {
[self.objects removeObject:key];
[self.objects insertObject:key atIndex:0];
}
[self.lock unlock];
return obj;
}
#pragma mark - Getter && Setter
- (NSMutableDictionary *)map {
if (_map == nil) {
_map = [NSMutableDictionary dictionary];
}
return _map;
}
- (NSMutableArray *)objects {
if (_objects == nil) {
_objects = [NSMutableArray arrayWithCapacity:0];
}
return _objects;
}
- (NSArray *)allItems {
NSMutableArray *tmpArray = [NSMutableArray arrayWithCapacity:0];
[_objects enumerateObjectsUsingBlock:^(NSString * _Nonnull key, NSUInteger idx, BOOL * _Nonnull stop) {
id obj = [self.map objectForKey:key];
if (obj) {
[tmpArray addObject:[self.map objectForKey:key]];
}
}];
return tmpArray.copy;
}
@end
-
_objects
是用来做排序的,在数据的插入、删除、读取操作的时候会同步更新这个数组,数组存放的是数据对应的key的值,你也可以包装一下存储包含key、value等其他属性的对象,这里只是演示就没有设计的很完善 -
_maps
用来存储数据源 - 实现也比较简单,按照开头介绍的规则去实现基本就没问题了,详细可以看代码
测试用例
- (void)testLRU {
LRUByArray *lru = [LRUByArray new];
lru.limitCount = 4;
for (NSInteger i = 0; i < 8; i ++) {
[lru addObject:@(i) forKey:@(i * 100).stringValue];
}
NSLog(@"all items = %@", lru.allItems);
/*
输出:
all items = (
7,
6,
5,
4
)
*/
[lru queryObjectByKey:@"400"];
NSLog(@"all items = %@", lru.allItems);
/*
输出:
all items = (
4,
7,
6,
5
)
*/
}
可以看到LRU已经实现了,当数据超过阀值会从数组的尾部开始删除数据知道满足限制条件;新插入或者读取的数据都会被放到数据的头部,这样就保证了从尾部开始淘汰数据,淘汰的就是最久未使用的数据了
使用链表的数据结构实现
你可能会说数组的方式不优雅,性能比不上链表的方式;而且一些开源的库,比如YYMemoryCache、Swift版本的NSCache的实现就采用了双向链表的方式来实现LRU的
- 插入数据,将数据设置为链表的头(数据已存在就移动到头部)
- 读取数据,将数据移动到链表的头部
- 删除数据,将数据从链表和数据源中移除
- 数据超过阀值,从链表的尾部逐一删除数据同时数据源删除数据
- 双向链表排序+字典存数据
实现代码
NS_ASSUME_NONNULL_BEGIN
@protocol LRUByLinkedListDelegate;
@interface LRUByLinkedList : NSObject
@property (nonatomic, assign) NSInteger limitCount;
@property (nonatomic, readonly, strong) NSArray *allItems;
@property (nonatomic, weak) id<LRUByLinkedListDelegate> delegate;
- (void)addObject:(id)obj forKey:(NSString *)key;
- (void)removeObjectByKey:(NSString *)key;
- (id)queryObjectByKey:(NSString *)key;
@end
@protocol LRUByLinkedListDelegate <NSObject>
- (void)willRemoveObject:(id)object;
- (void)didRemoveObject:(id)object;
@end
NS_ASSUME_NONNULL_END
#import "LRUByLinkedList.h"
@interface HCLinkedNode : NSObject {
@package
__unsafe_unretained HCLinkedNode *_pre;
__unsafe_unretained HCLinkedNode *_next;
NSString *_key;
id _value;
}
@end
@implementation HCLinkedNode
@end
@interface HCLinkedList : NSObject {
@package
NSMutableDictionary *_map;
HCLinkedNode *_head;
HCLinkedNode *_tail;
NSInteger _costCount;
}
- (void)insertNodeAtHead:(HCLinkedNode *)node;
- (void)bringNodeToHead:(HCLinkedNode *)node;
- (void)removeNode:(HCLinkedNode *)node;
- (HCLinkedNode *)removeTailNode;
- (void)removeAll;
@end
@implementation HCLinkedList
- (instancetype)init {
self = [super init];
if (self) {
_map = [NSMutableDictionary dictionary];
_costCount = 0;
}
return self;
}
// [pre][ Node ][next]
- (void)insertNodeAtHead:(HCLinkedNode *)node {
if (node == nil) {
return;
}
[_map setValue:node forKey:node->_key];
_costCount++;
if (_head) { // 头节点存在
node->_next = _head; // 将插入的节点的后缀节点指向头节点
_head->_pre = node; // 将头节点的前缀节点指向node
_head = node; // 设置头节点为插入的新的节点
} else { // 节点不存在,链表为空的时候
_head = _tail = node; // 将头尾都设置为首个节点
}
}
- (void)bringNodeToHead:(HCLinkedNode *)node {
if (node == nil) {
return;
}
if (_head == node) { // 元素本来就在头部
return;
}
if (_tail == node) { // 元素在尾部
_tail = node->_pre; // 将尾部设置为node的头
_tail->_next = nil; // 将尾部的后缀节点设置空
} else { // 元素在中间位置
node->_next->_pre = node->_pre; // 将node的后缀节点的前缀设置为node的前节点
node->_pre->_next = node->_next; // 将node的前缀节点的后缀设置为node的后节点
}
node->_next = _head; // 将node的后缀节点设置为之前的头
node->_pre = nil;
_head->_pre = node; // 将之前的头节点的前缀设置为node
_head = node; // 将头设置为首个节点
}
- (void)removeNode:(HCLinkedNode *)node {
if (node == nil) {
return;
}
[_map removeObjectForKey:node->_key]; // 删除数据源
_costCount--;
if (node->_next) { // node不为尾部节点
node->_next->_pre = node->_pre; // 将node的后缀节点的前缀设置为node的前缀节点
}
if (node->_pre) { // node不为头部节点
node->_pre->_next = node->_next; // 将node的前缀节点的后缀设置为node的后缀节点
}
if (_head == node) { // node为头
_head = node->_next; // 将头节点设置为node的后缀节点
}
if (_tail == node) { // node为尾
_tail = node->_pre; // 将尾节点设置为node的前缀
}
}
- (HCLinkedNode *)removeTailNode {
if (_tail == nil) {
return nil;
}
HCLinkedNode *tail = _tail;
[_map removeObjectForKey:tail->_key];
_costCount--;
if (_head == _tail) {
_head = _tail = nil;
} else {
_tail->_pre->_next = nil;
_tail = _tail->_pre;
}
return tail;
}
- (void)removeAll {
_head = _tail = nil;
_costCount = 0;
[_map removeAllObjects];
}
@end
@interface LRUByLinkedList ()
@property (nonatomic, strong) HCLinkedList *lru;
@property (nonatomic, strong) NSLock *lock;
@end
@implementation LRUByLinkedList
- (instancetype)init {
self = [super init];
if (self) {
_lru = [HCLinkedList new];
_limitCount = 4;
_lock = [NSLock new];
}
return self;
}
- (void)addObject:(id)obj forKey:(nonnull NSString *)key {
if (key == nil) {
return;
}
if (obj == nil) {
[self.lock lock];
HCLinkedNode *node = [self.lru->_map valueForKey:key];
if (node) {
if (self.delegate && [self.delegate respondsToSelector:@selector(willRemoveObject:)]) {
[self.delegate willRemoveObject:node->_value];
}
[self.lru removeNode:node];
if (self.delegate && [self.delegate respondsToSelector:@selector(didRemoveObject:)]) {
[self.delegate didRemoveObject:node->_value];
}
}
[self.lock unlock];
return;
}
[self.lock lock];
// 插入数据
HCLinkedNode *existNode = [self.lru->_map objectForKey:key];
if (existNode) {
[self.lru bringNodeToHead:existNode];
} else {
HCLinkedNode *node = [HCLinkedNode new];
node->_key = key;
node->_value = obj;
[self.lru insertNodeAtHead:node];
}
// 判断是否超出限制
if (self.lru->_costCount > self.limitCount) {
NSInteger costCount = self.lru->_costCount;
while (costCount > self.limitCount) {
if (self.delegate && [self.delegate respondsToSelector:@selector(willRemoveObject:)]) {
[self.delegate willRemoveObject:self.lru->_tail];
}
HCLinkedNode *removedNode = [self.lru removeTailNode];
if (self.delegate && [self.delegate respondsToSelector:@selector(didRemoveObject:)]) {
[self.delegate didRemoveObject:removedNode->_value];
}
costCount--;
}
}
[self.lock unlock];
}
- (void)removeObjectByKey:(NSString *)key {
if (key == nil) {
return;
}
[self.lock lock];
HCLinkedNode *node = [self.lru->_map objectForKey:key];
if (node) {
if (self.delegate && [self.delegate respondsToSelector:@selector(willRemoveObject:)]) {
[self.delegate willRemoveObject:node->_value];
}
[self->_lru removeNode:node];
if (self.delegate && [self.delegate respondsToSelector:@selector(didRemoveObject:)]) {
[self.delegate didRemoveObject:node->_value];
}
}
[self.lock unlock];
}
- (id)queryObjectByKey:(NSString *)key {
HCLinkedNode *node = [self.lru->_map objectForKey:key];
if (node) {
[self.lru bringNodeToHead:node];
}
return node ? node->_value : nil;
}
#pragma mark - Private Method
#pragma mark - Getter && Setter
- (NSArray *)allItems {
NSMutableArray *tmpArray = [NSMutableArray arrayWithCapacity:0];
HCLinkedNode *node = self.lru->_head;
while (node) {
[tmpArray addObject:node->_value];
node = node->_next;
}
return tmpArray.copy;
}
@end
@interface HCLinkedNode : NSObject {
@package
__unsafe_unretained HCLinkedNode *_pre;
__unsafe_unretained HCLinkedNode *_next;
NSString *_key;
id _value;
}
HCLinkedNode
这个结构包含pre、next节点、同时保存了数据的key和value;用于作为链表的node
@interface HCLinkedList : NSObject {
@package
NSMutableDictionary *_map;
HCLinkedNode *_head;
HCLinkedNode *_tail;
NSInteger _costCount;
}
@end
-
HCLinkedList
包含一个map用来保存数据,head和tail分别来记录链表的头和尾(也就是最近使用和最久未使用的数据)、costCount记录链表的长度 - 同时提供了链表的操作方法
insertNodeAtHead
、bringNodeToHead
、removeNode
、removeTailNode
来供不同的场景将数据源封装成node放在链表合适的位置;具体实现看代码有注释
测试用例
- (void)testLRUByLinkedList {
LRUByLinkedList *lru = [LRUByLinkedList new];
lru.limitCount = 4;
lru.delegate = self;
for (NSInteger i = 0; i < 8; i ++) {
[lru addObject:@(i) forKey:@(i * 100).stringValue];
}
NSLog(@"all items = %@", lru.allItems);
/*
输出:
all items = (
7,
6,
5,
4
)
*/
[lru queryObjectByKey:@"400"];
NSLog(@"all items = %@", lru.allItems);
/*
输出:
all items = (
4,
7,
6,
5
)
*/
[lru addObject:@(8) forKey:@(8 * 100).stringValue]; // -[ViewController didRemoveObject:] 5
}
- (void)willRemoveObject:(id)object {
}
- (void)didRemoveObject:(id)object {
NSLog(@"%s %@", __FUNCTION__, object);
}
看到输出结果也符合我们的预期
总结
我们在数据缓存的时候,往往会用到一些淘汰算房,常用的就是LRU、LFU;实现这两个算法的方式也可以多式多样;这里也是使用了比较常用的两种方式去实现了一下,同时学习了一下链表的结构和使用
网友评论