Flutter动态缓存管理插件dart_dyncache的使用

发布于 1周前 作者 zlyuanteng 来自 Flutter

Flutter 动态缓存管理插件 dart_dyncache 的使用

DynCache 是一个用于在 Dart 中处理所有缓存需求的动态和灵活库。

DynCache 旨在存储资源或时间密集型方法(如数据库查询)的结果。当缓存中的元素被移除时,它们会立即被垃圾回收。例如,当调用 set 方法且 length 等于 cacheCapacity 时。

特性

  • 有序存储(加权键值对)
  • 支持驱逐回调、自定义条目权重和辅助键的缓存实现
  • 实现了 FIFO、LRU 和 LFU 缓存

辅助键

在缓存对象时,你可能希望使用两个不同的键来查询同一个对象。在初始化缓存时,你可以选择添加 <code>AuxiliaryKeyManager</code>,这是一种将一个键映射到另一个键的方法。

给定的 <code>AuxiliaryKeyManager</code> 所有可能的辅助键集不应与所有可能的主要键集相交。主要键和辅助键之间应该有一一对应的关系。根据辅助键和值,应该可以恢复主要键,反之亦然。

使用

基本缓存使用

final cache = FIFODynamicCache<int, String>(10,
    storageGenerator: <K, V>(entryWeight) => 
        OrderedMap(entryWeight: entryWeight));

cache.set(1, 'val1');
cache.get(1); // 'val1'
cache.get(2); // null

for (int i = 10; i < 20; i++) {
    cache.set(i, 'val$i');
}
cache.get(1); // null

一个 LFU 缓存实现

final cache = FIFODynamicCache<int, String>(10,
    entryWeight: (key, value, accessWeight, accessesSinceLastHit) => accessWeight - (accessesSinceLastHit / 1000),
    storageGenerator: <K, V>(entryWeight) => 
        OrderedMap(entryWeight: entryWeight));

辅助键映射

final manager = AuxiliaryKeyManager<String, int, String>(
    generateAuxiliaryKeyFromMainKeyAndValue: (mainKey, value) => 
        '$mainKey',
    generateMainKeyFromAuxiliaryKeyAndValue: (auxiliaryKey, value) => 
        int.parse(auxiliaryKey));

final cache = FIFODynamicCache<int, String>(10,
    storageGenerator: <K, V>(entryWeight) => 
        OrderedMap(entryWeight: entryWeight),
    auxiliaryKeyManagers: [manager]);

cache.set(1, 'val1');
cache.get(1);   // 'val1'
cache.get('1'); // 'val1'

cache.set('2', 'val2');
cache.get(2);   // 'val2'
cache.get('2'); // 'val2'

驱逐回调

void callback(int key, String value) {
    print('$key:$value');
}

final cache = FIFODynamicCache<int, String>(10,
    storageGenerator: <K, V>(entryWeight) => 
        OrderedMap(entryWeight: entryWeight),
    onEvict: callback);

cache.set(1, 'val1');
cache.remove(1); // '1:val1'

cache.set(2, 'val2');
for (int i = 3; i < 11; i++) {
    cache.set(i, '');
}
// '2:val2'

完整示例

以下是一个完整的示例,展示了如何使用 dart_dyncache 插件。

import 'dart:math';

import 'package:dart_dyncache/dart_dyncache.dart';

void main() async {
  // 一个标准的 LFU 缓存
  final lfuCache = LFUDynamicCache<int, int>(10,
      storageGenerator: <K, V>(p0) => OrderedMap(entryWeight: p0),
      onEvict: (p0, p1) {
        if (p0 == 7 || p0 == -1) print('removed $p0');
      });

  // 填充垃圾数据
  for (int i = 0; i < 10; i++) {
    lfuCache.set(-i, 0);
  }

  print('0: ${lfuCache.values}');

  // 并且它会被替换,被驱逐的项目会调用 `onEvict` 方法(如果提供了的话)
  for (int i = 0; i < 10; i++) {
    lfuCache.set(i, i);
  }

  // 进行一些访问
  lfuCache.get(5);
  lfuCache.get(5);
  lfuCache.get(4);
  lfuCache.get(4);
  lfuCache.get(4);
  lfuCache.get(5);

  // 移除一个值,这会调用 `onEvict` 回调(如果提供了的话)
  lfuCache.remove(7);

  print('1: ${lfuCache.values}');

  // 被访问过的键值对将继续被缓存,因为它们现在权重更高
  for (int i = 0; i < 10; i++) {
    lfuCache.set(i + 10, 0);
  }
  print('2: ${lfuCache.values}');

  // 如果我们想要,我们也可以基于元素上次被访问的时间为其添加权重
  final lfruCache = SimpleDynamicCache<int, int>(10,
      entryWeight: (key, value, accessWeight, accessesSinceLastHit) => 
          accessWeight - (accessesSinceLastHit / 10),
      storageGenerator: <K, V>(p0) => OrderedMap(entryWeight: p0));
  for (int i = 0; i < 10; i++) {
    lfruCache.set(i, i);
  }
  lfruCache.get(5);
  lfruCache.get(5);
  lfruCache.get(4);
  lfruCache.get(4);
  lfruCache.get(4);
  lfruCache.get(5);

  // 如果插入更多键...
  for (int i = 0; i < 10; i++) {
    lfruCache.set(-i, 0);
  }

  // 被访问过的键值对仍然存在
  print('3: ${lfruCache.values}');

  // 但如果继续...
  for (int i = 0; i < 20; i++) {
    lfruCache.set(-i, 0);
  }

  // 你可以看到它们的权重降低了
  print('4: ${lfruCache.values}');

  for (int i = 0; i < 10; i++) {
    lfruCache.set(-i, 0);
  }
  // 经过一段时间后,所有的键值对都消失了
  print('5: ${lfruCache.values}');

  // 你也可以用 `AsyncBasicDynamicCache` 包装一个 `BasicDynamicCache` 来支持内置的未来支持。

  // 你应该只使用 `getAsync` 与 `AsyncBasicDynamicCache`。
  // `get` 将会错过所有关联了未来的键。
  // `getAsync` 会等待该未来的完成。
  final fifoCache = FIFODynamicCache<int, int>(10,
      storageGenerator: <K, V>(e) => OrderedMap(entryWeight: e));
  final asyncFifoCache = AsyncBasicDynamicCache(fifoCache);

  asyncFifoCache.setAsync(1, () async {
    await Future.delayed(Duration(milliseconds: 2));
    return 1;
  }());

  // 一个 get 会错过
  print('6: ${asyncFifoCache.get(1, throwOnUnsafe: false)}');
  // 但一个 asyncGet 会等待结果
  print('7: ${await asyncFifoCache.getAsync(1)}');

  // 你还可以向一个 `BasicDynamicCache` 添加一个 `AuxiliaryKeyManager`。
  // 一个 `AuxiliaryKeyManager` 为每个添加的主要键创建一个辅助键,反之亦然。
  // 主要键和辅助键之间应该有一一对应的关系。辅助键集合不应相交。

  // 这个管理器将类型为 `int` 的主要键转换为其字符串表示形式。
  // 它通过解析字符串来将辅助键转换为主要键。
  final manager = AuxiliaryKeyManager<String, int, String>(
      generateAuxiliaryKeyFromMainKeyAndValue: (mainKey, value) => '$mainKey',
      generateMainKeyFromAuxiliaryKeyAndValue: (auxiliaryKey, value) => 
          int.parse(auxiliaryKey));
  final lruCache = LRUDynamicCache(10,
      storageGenerator: <K, V>(e) => OrderedMap(entryWeight: e),
      auxiliaryKeyManagers: [manager]);

  // 设置一个主要键...
  lruCache.set(1, 'val1');
  // 可以通过辅助键查询
  print('8: ${lruCache.get('1')}');
  // 并设置一个辅助键...
  lruCache.set('2', 'val2');
  // 可以通过相关的主键查询。
  print('9: ${lruCache.get(2)}');

  // 一个无效的辅助键仍然会失败。同样,不支持的键类型也会失败。
  print('10: ${lruCache.get('3')}, ${lruCache.get(Object())}');

  // 不支持的键类型会抛出异常。
  // 这包括未处理的类型。
  try {
    lruCache.set(Object(), 'objectVal');
  } catch (e) {
    print(e);
  }
  // 解析错误的键也会抛出异常。
  try {
    lruCache.set('not a int', 'bad parse');
  } catch (e) {
    print(e);
  }

  // 默认情况下,辅助键冲突会抛出错误,但如果你的方法是安全的,你可能希望将 `checkAuxiliaryKeys` 设置为 `false` 以避免不必要的计算

  final badManager1 = AuxiliaryKeyManager<int, int, String>(
      generateAuxiliaryKeyFromMainKeyAndValue: (mainKey, value) => mainKey,
      generateMainKeyFromAuxiliaryKeyAndValue: (auxiliaryKey, value) => 
          auxiliaryKey,
      id: 'aux keys and main keys are the same set');
  final badManager2 = AuxiliaryKeyManager<String, int, String>(
      generateAuxiliaryKeyFromMainKeyAndValue: (mainKey, value) => '$mainKey',
      generateMainKeyFromAuxiliaryKeyAndValue: (k, v) => int.parse(k),
      id: 'int to string 1');
  final badManager3 = AuxiliaryKeyManager<String, int, String>(
      generateAuxiliaryKeyFromMainKeyAndValue: (mainKey, value) => '$mainKey',
      generateMainKeyFromAuxiliaryKeyAndValue: (k, v) => int.parse(k),
      id: 'int to string 2');
  final badCache1 = LRUDynamicCache(10,
      storageGenerator: <K, V>(e) => OrderedMap(entryWeight: e),
      auxiliaryKeyManagers: [badManager1]);
  final badCache2 = LRUDynamicCache(10,
      storageGenerator: <K, V>(e) => OrderedMap(entryWeight: e),
      auxiliaryKeyManagers: [badManager2, badManager3]);

  try {
    badCache1.set(1, 'aux conflicts with main');
  } catch (e) {
    print(e);
  }
  try {
    badCache2.set('1', 'aux conflicts with aux');
  } catch (e) {
    print(e);
  }

  // !!! 注意关于使用辅助键与异步缓存的重要事项 !!!
  // 辅助键通常按如下方式工作:
  //         |----------&gt; +-------+ &gt;--------V |----------&gt; +-------+ &gt;--------V
  //  [AUXILIARY KEY]     | VALUE |     [MAIN KEY]          | VALUE |     [AUXILIARY KEY]
  //         ^----------&lt; +-------+ &lt;--------| ^----------&lt; +-------+ &lt;--------|
  //
  // 对于一个辅助键关联其他辅助键,它必须首先转换为主键。
  //
  // 问题在于,由于有未来,值直到未来完成才知道;当调用 `asyncGet` 时,它只会等待传入的键是否与 `asyncSet` 传入的键相同,否则即使它们是相关的主键或辅助键也会错过。
  //
  // 通过 `AuxiliaryKeyManager` 构造函数的 `valueNeeded` 参数可以解决此问题。如果管理器转换函数不依赖于值,可以将其设置为 `false`。

  final manager1 = AuxiliaryKeyManager<String, int, int>(
      generateAuxiliaryKeyFromMainKeyAndValue: (mainKey, value) => '$mainKey',
      generateMainKeyFromAuxiliaryKeyAndValue: (auxiliaryKey, value) => 
          int.parse(auxiliaryKey),
      valueNeeded: false);

  final manager2 = AuxiliaryKeyManager<double, int, int>(
      generateAuxiliaryKeyFromMainKeyAndValue: (mainKey, value) => 
          mainKey + value! / (pow(10, '${value}'.length)),
      generateMainKeyFromAuxiliaryKeyAndValue: (auxiliaryKey, value) => 
          auxiliaryKey ~/ 1);

  final cache = LRUDynamicCache<int, int>(10,
      storageGenerator: <K, V>(e) => OrderedMap(entryWeight: e),
      auxiliaryKeyManagers: [manager1, manager2]);
  final asyncCache = AsyncBasicDynamicCache(cache);

  // 这将值 1 设置为主要键 100。
  // manager1 将创建一个辅助键 '100' 并不需要值。
  // manager2 将创建一个辅助键 101 并需要值。
  asyncCache.setAsync(100, () async {
    await Future.delayed(Duration(milliseconds: 5));
    return 1;
  }());

  final futures1 = [
    asyncCache.getAsync(100),
    asyncCache.getAsync('100'),
    asyncCache.getAsync(100.1)
  ];
  final results1 = await Future.wait(futures1);
  // 主键命中,'100' 键命中因为它不需要值。
  // 因为 manager2 需要值,101 错过因为我们无法计算它在不知道值的情况下。
  print('11: $results1');

  asyncCache.setAsync(200.1, () async {
    await Future.delayed(Duration(milliseconds: 5));
    return 1;
  }());

  final futures2 = [
    asyncCache.getAsync(200),
    asyncCache.getAsync('200'),
    asyncCache.getAsync(200.1)
  ];
  // 因为 manager2 需要值,我们无法推导主键。
  // 因为我们无法推导主键,我们无法推导 manager1 的辅助键;只有 manager2 的键在知道值之前等待值。
  final results2 = await Future.wait(futures2);
  print('12: $results2');

  // 当然,在未来完成之后,所有键现在都是关联的。
  final futures3 = [
    asyncCache.getAsync(200),
    asyncCache.getAsync('200'),
    asyncCache.getAsync(200.1)
  ];
  final results3 = await Future.wait(futures3);
  print('13: $results3');
}

// kralverde (c) 2023

更多关于Flutter动态缓存管理插件dart_dyncache的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter动态缓存管理插件dart_dyncache的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,下面是一个关于如何使用 dart_dyncache 插件进行 Flutter 动态缓存管理的代码示例。dart_dyncache 是一个用于在 Flutter 应用中实现动态缓存管理的 Dart 库。它提供了缓存的存储、获取、失效等功能。

首先,确保你已经在 pubspec.yaml 文件中添加了 dart_dyncache 依赖:

dependencies:
  flutter:
    sdk: flutter
  dart_dyncache: ^最新版本号

然后运行 flutter pub get 以获取依赖。

接下来,我们来看一个具体的代码示例,展示如何使用 dart_dyncache

import 'package:flutter/material.dart';
import 'package:dart_dyncache/dart_dyncache.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final Dyncache _cache = Dyncache();

  @override
  void initState() {
    super.initState();
    // 设置全局缓存配置
    _cache.config = CacheConfig(
      maxCount: 100, // 最大缓存条数
      maxAge: 60, // 缓存有效时间(秒)
    );

    // 示例:从缓存中获取数据(如果缓存不存在,则模拟一个网络请求)
    _fetchData();
  }

  Future<void> _fetchData() async {
    String key = 'example_key';
    var cachedData = await _cache.get(key);

    if (cachedData == null) {
      // 模拟网络请求
      String newData = await _simulateNetworkRequest();
      // 将数据存入缓存
      await _cache.set(key, newData);
      // 更新UI(这里简单打印输出)
      print('Fetched new data: $newData');
    } else {
      // 从缓存中获取数据
      print('Fetched cached data: $cachedData');
    }
  }

  Future<String> _simulateNetworkRequest() async {
    // 模拟网络请求延迟
    await Future.delayed(Duration(seconds: 2));
    return 'This is fetched data from network';
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Dynamic Cache Management'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            // 每次点击按钮时尝试获取数据
            _fetchData();
          },
          child: Text('Fetch Data'),
        ),
      ),
    );
  }
}

在这个示例中,我们做了以下几件事:

  1. 配置缓存:在 initState 方法中,我们配置了全局缓存参数,包括最大缓存条数和缓存有效时间。
  2. 获取数据:在 _fetchData 方法中,我们尝试从缓存中获取数据。如果缓存中没有数据,则模拟一个网络请求并将获取的数据存入缓存。
  3. 模拟网络请求_simulateNetworkRequest 方法模拟了一个网络请求,这里只是简单地使用 Future.delayed 来模拟延迟。
  4. UI更新:在按钮点击事件中调用 _fetchData 方法来尝试获取数据,并打印输出。

这样,通过使用 dart_dyncache 插件,你可以轻松地在 Flutter 应用中实现动态缓存管理,提高数据获取的效率。

回到顶部