dio_cache_interceptor缓存拦截器框架之MemCacheStore、_LruMap源码分析(三)

227 阅读16分钟

MemCacheStore

class MemCacheStore extends CacheStore {

final _LruMap _cache;

/// [maxSize]: Total allowed size in bytes (7MB by default).
///
/// [maxEntrySize]: Allowed size per entry in bytes (500KB by default).
///
/// To prevent making this store useless, be sure to
/// respect the following lower-limit rule: maxEntrySize * 5 <= maxSize.
///
MemCacheStore({
  int maxSize = 7340032,
  int maxEntrySize = 512000,
}) : _cache = _LruMap(maxSize: maxSize, maxEntrySize: maxEntrySize);

}

MemCacheStore 是一个将响应保存在专用内存 LRU(Map) 中的缓存存储器类。

它是 CacheStore 的一个子类,用于实现缓存的存储和检索。缓存存储器用于保存网络请求的响应,以便后续使用。MemCacheStore 将缓存存储在内存中的 LRU(Map) 中,LRU 表示"最近最少使用",它会保持最近使用的缓存项,并在达到一定容量后,淘汰最少使用的缓存项。

这个类的主要属性和方法如下:

  • _cache: 一个 _LruMap 类的实例,用于存储缓存项。LRU Map 是一个最近最少使用的缓存策略,它会保持最近使用的缓存项,并在达到一定容量后,淘汰最少使用的缓存项。
  • MemCacheStore() 构造函数:可以接收两个可选参数 maxSizemaxEntrySize,用于指定缓存的总大小和每个缓存项的最大大小。默认情况下,maxSize 是 7340032 字节(7MB),maxEntrySize 是 512000 字节(500KB)。

在这个存储器中,缓存项的存储和检索都是非常快速的,因为它是直接存储在内存中的。但是,需要注意的是,内存是有限的,如果缓存的数据量很大,可能会导致内存占用过多。

clean

@override
Future<void> clean({
  CachePriority priorityOrBelow = CachePriority.high,
  bool staleOnly = false,
}) {
  final keys = <String>[];

  _cache.entries.forEach((key, resp) {
    var shouldRemove = resp.value.priority.index <= priorityOrBelow.index;
    shouldRemove &= (staleOnly && resp.value.isStaled()) || !staleOnly;

    if (shouldRemove) {
      keys.add(key);
    }
  });

  for (var key in keys) {
    _cache.remove(key);
  }

  return Future.value();
}

这是 MemCacheStore 类中的一个方法 clean,用于清理缓存项。

该方法接收两个可选参数:

  • priorityOrBelow:缓存项的优先级,所有低于等于指定优先级的缓存项都会被清理,默认为 CachePriority.high,即清理所有高于指定优先级的缓存项。
  • staleOnly:一个布尔值,表示是否只清理已过期的缓存项,默认为 false,即清理所有缓存项。

方法的执行步骤如下:

  1. 首先,定义一个空列表 keys,用于存储待删除的缓存项的键。
  2. 然后,遍历 _cache 中的每一个缓存项(以键值对的形式)。
  3. 对于每一个缓存项,检查它的优先级是否低于等于 priorityOrBelow 的优先级,以及它是否已过期(如果 staleOnlytrue),如果满足条件,则将其键加入到 keys 列表中。
  4. 最后,遍历 keys 列表,将对应的缓存项从 _cache 中移除,完成清理操作。
  5. 方法返回一个 Future<void>,表示清理操作的异步完成状态。

delete

@override
Future<void> delete(String key, {bool staleOnly = false}) {
  final resp = _cache.entries[key];
  if (resp == null) return Future.value();

  if (staleOnly && !resp.value.isStaled()) {
    return Future.value();
  }

  _cache.remove(key);

  return Future.value();
}

这是 MemCacheStore 类中的一个方法 delete,用于删除指定键的缓存项。

该方法接收两个可选参数:

  • staleOnly:一个布尔值,表示是否只删除已过期的缓存项,默认为 false,即删除指定键的缓存项,不管它是否过期。

方法的执行步骤如下:

  1. 首先,通过指定的键 key_cache 中查找对应的缓存项。

  2. 如果找不到该键对应的缓存项,直接返回一个已完成的 Future,表示删除操作完成(实际上什么都没做)。

  3. 如果找到了缓存项,则根据 staleOnly 参数的值来决定是否需要进一步处理:

    • 如果 staleOnlytrue,即只删除已过期的缓存项,并且当前缓存项未过期,直接返回一个已完成的 Future,表示删除操作完成(实际上什么都没做)。
    • 如果 staleOnlyfalse,或者当前缓存项已过期,将该键对应的缓存项从 _cache 中移除,完成删除操作。
  4. 方法返回一个 Future<void>,表示删除操作的异步完成状态。

deleteFromPath

@override
Future<void> deleteFromPath(
  RegExp pathPattern, {
  Map<String, String?>? queryParams,
}) async {
  final responses = await getFromPath(
    pathPattern,
    queryParams: queryParams,
  );

  for (final response in responses) {
    _cache.remove(response.key);
  }
}

这是 MemCacheStore 类中的另一个方法 deleteFromPath,用于根据路径模式和查询参数删除符合条件的缓存项。

该方法接收两个参数:

  • pathPattern:一个正则表达式,用于匹配缓存项的路径。
  • queryParams:一个可选的查询参数字典,用于进一步过滤匹配的缓存项。

方法的执行步骤如下:

  1. 首先,通过调用 getFromPath 方法来检索符合条件的缓存项。getFromPath 方法会返回一个包含匹配缓存项的列表。
  2. 然后,遍历返回的缓存项列表,逐个从 _cache 中移除匹配的缓存项。

exists

@override
Future<bool> exists(String key) {
  return Future.value(_cache.entries.containsKey(key));
}

这是 MemCacheStore 类中的 exists 方法,用于检查给定的缓存键是否存在于缓存中。

该方法接收一个参数:

  • key:要检查的缓存键。

方法的执行步骤如下:

  1. 首先,通过 _cache.entries 属性获取当前缓存中所有的键值对。_cache.entries 返回一个 Map,其中键是缓存键,值是对应的缓存响应(CacheResponse 对象)。
  2. 然后,通过 containsKey 方法检查给定的 key 是否存在于缓存中。containsKey 方法会返回一个布尔值,指示缓存是否包含指定的键。
  3. 最后,将步骤 2 中的结果包装在 Future.value 中,并返回。

get

@override
Future<CacheResponse?> get(String key) async {
  return _cache[key];
}

这是 MemCacheStore 类中的 get 方法,用于从缓存中获取指定缓存键对应的缓存响应(CacheResponse 对象)。

该方法接收一个参数:

  • key:要获取缓存响应的缓存键。

方法的执行步骤如下:

  1. 首先,通过 _cache[key] 表达式从缓存中获取指定缓存键对应的缓存响应。
  2. 然后,直接将缓存响应返回。

getFromPath

@override
Future<List<CacheResponse>> getFromPath(
  RegExp pathPattern, {
  Map<String, String?>? queryParams,
}) async {
  final responses = <CacheResponse>[];

  for (final entry in _cache.entries.entries) {
    final resp = entry.value.value;
    if (pathExists(resp.url, pathPattern, queryParams: queryParams)) {
      responses.add(resp);
    }
  }

  return responses;
}

这是 MemCacheStore 类中的 getFromPath 方法,用于根据路径模式和查询参数从缓存中获取匹配的缓存响应列表。

该方法接收两个参数:

  • pathPattern:路径匹配的正则表达式。
  • queryParams:查询参数,用于进一步过滤匹配的缓存响应(可选参数)。

方法的执行步骤如下:

  1. 首先,创建一个空的 List<CacheResponse> 类型的列表 responses,用于存储匹配的缓存响应。
  2. 然后,遍历 _cache.entries 中的所有缓存项。
  3. 对于每个缓存项,从中获取缓存响应对象 resp
  4. 接着,调用 pathExists 方法,判断 resp.url 是否与指定的路径模式和查询参数匹配。
  5. 如果匹配成功,则将该缓存响应对象 resp 添加到 responses 列表中。
  6. 最后,将 responses 列表返回。

set

@override
Future<void> set(CacheResponse response) {
  _cache.remove(response.key);
  _cache[response.key] = response;

  return Future.value();
}

这是 MemCacheStore 类中的 set 方法,用于将缓存响应存储到缓存中或更新现有的缓存响应。

该方法接收一个 CacheResponse 对象作为参数,表示要存储或更新的缓存响应。

方法的执行步骤如下:

  1. 首先,根据传入的缓存响应对象 responsekey 属性从 _cache 中移除可能已经存在的同名缓存项。这样可以确保每个缓存响应的 key 是唯一的。
  2. 然后,将传入的缓存响应对象 response 存储到 _cache 中,以 response.key 作为键。
  3. 最后,返回一个 Future 对象,表示缓存操作完成。

close

@override
Future<void> close() {
  _cache.clear();
  return Future.value();
}

这是 MemCacheStore 类中的 close 方法,用于释放底层资源(如果有的话)并清空缓存。

该方法没有接收任何参数,也没有返回值。它的主要作用是清空 _cache 中的所有缓存项,以便在不再需要缓存数据时释放内存。

方法的执行步骤如下:

  1. 调用 _cacheclear 方法,将所有缓存项从缓存中移除,从而释放内存。
  2. 返回一个 Future 对象,表示操作已完成。

_LruMap

class _LruMap {
  _Link? _head;
  _Link? _tail;

  final entries = <String, _Link>{};

  int _currentSize = 0;
  final int maxSize;
  final int maxEntrySize;

  _LruMap({required this.maxSize, required this.maxEntrySize}) {
    assert(maxEntrySize != maxSize);
    assert(maxEntrySize * 5 <= maxSize);
  }
}

这是 _LruMap 类的实现,它是 MemCacheStore 类中用于缓存数据的内部私有类。它实现了一个 LRU(最近最少使用)缓存算法,用于存储缓存项,并在缓存容量达到上限时自动淘汰最不常用的缓存项。

属性和构造函数的解释如下:

  1. _head_tail:分别表示链表的头和尾节点。链表是用于实现 LRU 算法的数据结构,通过将最近使用的缓存项移动到链表头,实现对最不常用的缓存项的淘汰。
  2. entries:一个映射(Map)数据结构,用于存储缓存项的键值对。键是缓存项的键(通常是请求的 URL),值是一个 _Link 对象,表示缓存项在链表中的位置。
  3. _currentSize:当前缓存的大小,以字节为单位。初始值为 0,随着缓存项的添加和淘汰,该值会动态更新。
  4. maxSize:缓存的最大容量,以字节为单位。当缓存大小超过该值时,会触发淘汰策略。
  5. maxEntrySize:单个缓存项的最大大小,以字节为单位。当单个缓存项的大小超过该值时,会触发淘汰策略。

构造函数接收 maxSizemaxEntrySize 两个参数,并进行了一些断言校验,确保 maxEntrySize 不等于 maxSize,并且满足条件 maxEntrySize * 5 <= maxSize,以避免在设置不合理的缓存大小时出现问题。

_LruMap 类实现了一个 LRU 链表结构,并提供了一些方法用于操作缓存项,例如添加、获取、删除等。在 MemCacheStore 中,_LruMap 用于存储缓存项,并在需要淘汰缓存时,按照 LRU 算法的原则,自动删除最不常用的缓存项,以确保缓存大小不超过设定的上限。

CacheResponse? operator [](String key)

/// []:用于访问列表或映射中的元素。
CacheResponse? operator [](String key) {
  final entry = entries[key];
  if (entry == null) return null;

  _moveToHead(entry);
  return entry.value;
}

这是 _LruMap 类中的 operator [] 方法的实现。该方法重载了 [] 运算符,使得可以像访问列表或映射一样,通过键值 key 来访问 _LruMap 中缓存项的值 CacheResponse

方法解释如下:

  1. CacheResponse? operator [](String key) {...}:这里定义了 [] 运算符的重载方法,接收一个 String 类型的 key 作为参数,表示要访问的缓存项的键。
  2. final entry = entries[key];:根据给定的 keyentries 映射中获取缓存项的 _Link 对象。如果找不到对应的键值,则返回 null
  3. if (entry == null) return null;:如果没有找到对应的缓存项,直接返回 null,表示缓存中没有该项。
  4. _moveToHead(entry);:如果找到了对应的缓存项,调用 _moveToHead 方法将该项移动到链表头部。这一步是为了实现 LRU 算法,将最近使用的缓存项放在链表头部,以便后续访问时能够更快地找到。
  5. return entry.value;:返回缓存项的值 CacheResponse

operator [] 方法中,首先查找给定 key 对应的缓存项是否存在。如果存在,将该缓存项移动到链表头部,并返回缓存项的值。如果不存在,则返回 null 表示缓存中没有该项。通过这种方式,可以方便地通过键值来访问缓存项,类似于访问列表或映射的方式。

operator []=(String key, CacheResponse resp)

/// []=:用于设置列表或映射中的元素。
void operator []=(String key, CacheResponse resp) {
  final entrySize = _computeSize(resp);
  // Entry too heavy, skip it
  if (entrySize > maxEntrySize) return;

  final entry = _Link(key, resp, entrySize);

  entries[key] = entry;
  _currentSize += entry.size;
  _moveToHead(entry);

  while (_currentSize > maxSize) {
    assert(_tail != null);
    remove(_tail!.key);
  }
}

这是 _LruMap 类中的 operator []= 方法的实现。该方法重载了 []= 运算符,用于向 _LruMap 中设置列表或映射的元素,即缓存项 CacheResponse

方法解释如下:

  1. void operator []=(String key, CacheResponse resp) {...}:这里定义了 []= 运算符的重载方法,接收一个 String 类型的 key 和一个 CacheResponse 类型的 resp 作为参数,表示要设置的缓存项的键和值。
  2. final entrySize = _computeSize(resp);:计算要设置的缓存项 resp 的大小。
  3. if (entrySize > maxEntrySize) return;:如果要设置的缓存项的大小超过了 maxEntrySize,则跳过不设置该项。这是为了避免将过大的缓存项添加到 _LruMap 中,保证缓存项大小不会超过设置的最大值。
  4. final entry = _Link(key, resp, entrySize);:根据给定的 keyresp 和计算出的大小 entrySize 创建一个 _Link 对象,表示要添加的缓存项。
  5. entries[key] = entry;:将新创建的缓存项 _Link 添加到 entries 映射中,以 key 作为键。
  6. _currentSize += entry.size;:将当前缓存大小 _currentSize 增加新缓存项的大小 entry.size
  7. _moveToHead(entry);:将新缓存项移动到链表头部,表示该项是最近访问的项,实现 LRU 算法。
  8. while (_currentSize > maxSize) {...}:如果当前缓存大小 _currentSize 超过了 maxSize,则进行缓存清理,以保证缓存大小不会超过设置的最大值。
  9. remove(_tail!.key);:在进行缓存清理时,移除链表尾部的缓存项,直到当前缓存大小 _currentSize 不再超过 maxSize

operator []= 方法用于设置缓存项,首先计算要设置的缓存项的大小,如果大小超过了 maxEntrySize,则跳过不设置该项。否则,创建一个新的 _Link 对象表示缓存项,并将该项添加到 entries 映射中。然后,将当前缓存大小 _currentSize 增加新缓存项的大小,并将新缓存项移动到链表头部。最后,如果当前缓存大小 _currentSize 超过了 maxSize,则进行缓存清理,移除链表尾部的缓存项,直到当前缓存大小不再超过 maxSize。通过这种方式,可以有效地管理缓存项,并保持缓存大小在一定范围内。

clear

void clear() {
  entries.clear();

  _head = null;
  _tail = null;
  _currentSize = 0;
}

clear() 方法用于清空 _LruMap 中的所有缓存项,即清空 entries 映射,并将链表头部 _head 和尾部 _tail 设置为 null,同时将当前缓存大小 _currentSize 设置为 0。

remove

CacheResponse? remove(String key) {
  final entry = entries[key];
  if (entry == null) return null;

  _currentSize -= entry.size;
  entries.remove(key);

  if (entry == _tail) {
    _tail = entry.next;
    _tail?.previous = null;
  }
  if (entry == _head) {
    _head = entry.previous;
    _head?.next = null;
  }

  return entry.value;
}

remove(String key) 方法用于从 _LruMap 中移除指定键的缓存项,并返回被移除的缓存项的值 CacheResponse?

方法首先检查指定键是否存在于 entries 映射中,如果不存在,则返回 null。如果指定键存在,就从 entries 映射中移除该缓存项,并将当前缓存大小 _currentSize 减去被移除项的大小 entry.size。然后,根据被移除项在链表中的位置,更新链表的头部 _head 和尾部 _tail。如果被移除项是链表的尾部,则将 _tail 指向其下一个节点 entry.next,并将下一个节点的前一个节点指针 previous 设置为 null。如果被移除项是链表的头部,则将 _head 指向其前一个节点 entry.previous,并将前一个节点的下一个节点指针 next 设置为 null

最后,方法返回被移除缓存项的值 entry.value。这样,通过调用 remove() 方法,可以从 _LruMap 中移除指定键的缓存项,并且链表的头部和尾部也会相应地更新,以保持链表的正确性和缓存项的顺序。

_moveToHead

void _moveToHead(_Link link) {
  if (link == _head) return;

  if (link == _tail) {
    _tail = link.next;
  }

  if (link.previous != null) {
    link.previous!.next = link.next;
  }
  if (link.next != null) {
    link.next!.previous = link.previous;
  }

  _head?.next = link;
  link.previous = _head;
  _head = link;
  _tail ??= link;
  link.next = null;
}

_moveToHead(_Link link) 方法用于将指定的链表节点 _Link 移动到链表头部。这是 LRU (Least Recently Used) 缓存淘汰策略的一部分,用于在访问或更新链表节点时,将最近访问的节点移动到链表头部,以保持缓存项的访问顺序。

方法首先检查指定的链表节点 link 是否已经是链表的头部 _head,如果是,则直接返回,不进行移动操作。

如果指定的链表节点 link 是链表的尾部 _tail,则需要更新 _taillink 的下一个节点 link.next。因为 _tail 存储的是链表中最少访问的节点,而如果将 link 移动到链表头部,它就成为了最近访问的节点,所以 _tail 需要指向其下一个节点。

然后,方法会将 link 从链表中断开,并将其移动到链表头部。具体操作如下:

  1. 如果 link 有前一个节点 link.previous,则将前一个节点的下一个节点指针 next 指向 link 的下一个节点 link.next。这样,就将 link 从链表中断开,它的前一个节点不再指向它。
  2. 如果 link 有下一个节点 link.next,则将下一个节点的前一个节点指针 previous 指向 link 的前一个节点 link.previous。这样,就将 link 从链表中断开,它的下一个节点不再指向它。
  3. 将链表头部 _head 的下一个节点指针 next 指向 link,将 link 的前一个节点指针 previous 指向当前的链表头部 _head。这样,link 成为了新的链表头部。
  4. 更新链表头部 _headlink,并将链表尾部 _tail 初始化为 link(如果当前 _tail 为空,则 _tail 会被设置为 link)。由于 link 已经在链表头部,所以它的下一个节点指针 next 设置为 null,表示它是链表的最前面一个节点。

通过调用 _moveToHead() 方法,可以将指定的链表节点移动到链表头部,从而实现 LRU 缓存淘汰策略,保持最近访问的缓存项在链表头部。这样,在缓存项超过最大限制时,可以优先淘汰链表尾部的节点,即最少访问的节点。

_computeSize

int _computeSize(CacheResponse resp) {
  var size = resp.content?.length ?? 0;
  size += resp.headers?.length ?? 0;

  return size;
}

_computeSize(CacheResponse resp) 方法用于计算缓存响应的大小,即响应的内容大小和头部大小的总和。

方法首先定义一个变量 size,并将其初始化为响应的内容大小 resp.content?.length。如果响应的内容不为 null,则返回其长度,否则返回 0。

接下来,将头部的大小 resp.headers?.length 加到 size 上。如果响应的头部不为 null,则返回其长度,否则返回 0。

最后,返回 size,即响应的内容大小和头部大小的总和。

_Link

class _Link implements MapEntry<String, CacheResponse> {
  _Link? next;
  _Link? previous;

  final int size;

  @override
  final String key;

  @override
  final CacheResponse value;

  _Link(this.key, this.value, this.size);
}

_Link 类实现了 MapEntry<String, CacheResponse> 接口,用于表示一个链表节点,每个节点都包含一个键值对以及链表中的前后节点。

_Link 类包含了四个成员变量:

  1. next:指向链表中下一个节点的指针。
  2. previous:指向链表中上一个节点的指针。
  3. size:表示该节点对应缓存响应的大小,即缓存响应内容和头部的总大小。
  4. key:缓存的键,用于在缓存中唯一标识该节点。
  5. value:缓存的响应,即缓存中实际存储的值。

参考资料

LRU 缓存