前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Flutter中利用MapCache加sqflite实现一个伪LRU三级缓存

Flutter中利用MapCache加sqflite实现一个伪LRU三级缓存

原创
作者头像
brzhang
发布2018-12-02 10:05:24
3.5K0
发布2018-12-02 10:05:24
举报
文章被收录于专栏:玩转全栈

在做flutter应用的时候,遇到了一个问题,纯粹属于自己给自己加戏,问题是什么呢?我的app首页是一个列表,目前每次进应用,都是通过网络拿到新的列表,所以,如果没有网络了,就看到了一个菊花,这样的用户体验可能并不怎么好吧,因此,这块的化,想给自己挖一个坑,让自己填一下,本来以为是一个非常简单的问题,因为如果是在Android平台上,用DiskLruCache,很容易就实现了这个需求啦。然而不信的是,经过我的调研,flutter仓库中的库不太符合要求。

首先,我列一下自己的需求

1、网络请求,我使用的是dio框架,在其上面稍微封装了一下,我的想法是需要在onSuccess回调中把get请求缓存下来,就像下面这样:

缓存
缓存

2、然后,在需要的地方,我需要判断缓存是否可用,如果可用,我就直接返回了,不发起网络请求,或者说,返回,并且发起网络请求,这依赖于业务需求,先不说这么多,大概方式是:

load缓存
load缓存

其中红框中的就是我通过key去缓存中查。

3、假如说,我们把接口定义成这样的,那么背后的实现,我们准备如何去做,首先,我是这么考虑的,写缓存,要先写到内存缓存,在写到磁盘缓存,在写的过程中,要使用新的替换旧的,磁盘缓存,和内存缓存都也要有大小的显示,所谓的lru就体现在这里了。

4、好,说来说去,只要有lru_cache就够了,但是,flutter官方仓库中似乎是没有的。自己写一个,似乎代价太大。那么简单模拟实现有没有,我想到了一个思路。

5、MapCache作为内存缓存,sqflite作为磁盘缓存,那么好,LRU怎么实现呢?我的思路是给value加上一个时间戳,当,数据操作一定范围是,将时间戳交旧的删掉,然后重新load内存缓存就ok啦,你一定看出来了,这个太暴力了。

具体的实现代码

1、CacheManger作为cache管理工具,我把它做成了单例,初始化的时候,把磁盘缓存加到了内存中。

代码语言:javascript
复制
import 'package:app/model/cache_object.dart';
import 'package:quiver/cache.dart';

class CacheManger {
  static final CacheManger _singleton = CacheManger._internal();

  CacheDataProvider _cacheDataProvider;

  MapCache<String, String> _cacheMap = MapCache();

  bool _avaiable = false;

  factory CacheManger() {
    return _singleton;
  }

  CacheManger._internal() {
    _cacheDataProvider = CacheDataProvider();
    _initMemoryCache();
  }

  Future<String> get(String key) async {
    if (!_avaiable) {
      await _initMemoryCache();
    }
    return _cacheMap.get(key);
  }

  Future set(String key, String value) async {
    _cacheMap.set(key, value); //写到内存
    //写到磁盘
    return _cacheDataProvider.set(CacheObject(key: key, value: value));
  }

  ///哈哈,假装在lru,偷懒实现
  Future lru() async {
    await _cacheDataProvider.lru();
    _initMemoryCache();
  }

  ///整个清理
  Future clear() async {
    await _cacheDataProvider.clear();
//    _initMemoryCache();
  }

  ///将磁盘缓存load到内存中来
  ///
  Future _initMemoryCache() async {
    List<CacheObject> cacheObjects = await _cacheDataProvider.getAll();
    for (var value in cacheObjects) {
      _cacheMap.set(value.key, value.value);
    }
    _avaiable = true;
  }
}

2、CacheDataProvider作为磁盘缓存操作的具体实现类,主要是一些数据库的操作,以及偷懒的LRU实现:

代码语言:javascript
复制
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';

///缓存数据库名字
const String dbName = "data.db";

///缓存表名字
final String tableCache = "table_cache";

///字段
final String columnId = "id";
final String columnKey = "key";
final String columnValue = "value";
final String columnTime = "time";

class CacheObject {
  int id;
  String key;
  String value;
  int time;

  CacheObject({this.id, this.key, this.value, this.time});

  CacheObject.fromJson(Map<String, dynamic> json) {
    id = json['id'];
    key = json['key'];
    value = json['value'] ?? "";
    time = json['time'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['id'] = this.id;
    data['key'] = this.key;
    data['value'] = this.value;
    data['time'] = this.time;
    return data;
  }
}

class CacheDataProvider {
  Database _db;

  ///操作db之前必须保证db是打开的
  Future _open({String name = dbName}) async {
    if (_db == null || !_db.isOpen) {
      var databasesPath = await getDatabasesPath();
      String path = join(databasesPath, name);
      _db = await openDatabase(path, version: 1,
          onCreate: (Database db, int version) async {
        await db.execute('''
create table $tableCache ( 
  $columnId integer primary key autoincrement, 
  $columnKey text not null,
  $columnValue text DEFAULT '{}',
  $columnTime integer not null,
  UNIQUE($columnKey)
  )
''');
      });
    }
  }

  ///设置待缓存对象,如果key重复,会直接替换
  Future<CacheObject> set(CacheObject cacheObject) async {
    await _open();
    List<Map> maps = await _db.query(tableCache,
        columns: [columnKey, columnValue],
        where: "$columnKey = ?",
        whereArgs: [cacheObject.key]);
    if (maps.length > 0) {
      int count = await _db.rawUpdate(
          'UPDATE $tableCache SET $columnValue = ?, $columnTime = ? WHERE $columnKey = ?',
          [
            cacheObject.value,
            new DateTime.now().millisecondsSinceEpoch ~/ 1000,
            cacheObject.key
          ]);
      print("updated: $count");
    } else {
      cacheObject.id = await _db.execute('''
    INSERT  INTO $tableCache($columnKey,$columnValue,$columnTime)
VALUES('${cacheObject.key}','${cacheObject.value}',strftime('%s','now'));
    ''');
    }
    return cacheObject;
  }

  ///取到缓存中的对象
  Future<CacheObject> get(String key) async {
    await _open();
    List<Map> maps = await _db.query(tableCache,
        columns: [columnKey, columnValue],
        where: "$columnKey = ?",
        whereArgs: [key]);
    if (maps.length > 0) {
      return new CacheObject.fromJson(maps.first);
    }
    return null;
  }

  ///取到缓存中的对象
  Future<List<CacheObject>> getAll() async {
    await _open();
    List<Map> maps = await _db.query(tableCache);
    if (maps.length > 0) {
      return maps.map((json) => CacheObject.fromJson(json)).toList();
    }
    return List();
  }

  ///简单的替换一下lru策略
  Future lru() async {
    await _open();
    List<Map> maps = await _db.query(tableCache);
    if (maps.length > 100) {
      var time = CacheObject.fromJson(maps[100]).time;
      return _db
          .delete(tableCache, where: "$columnTime <= ?", whereArgs: [time]);
    }
  }

  ///整个清理
  Future<int> clear() async {
    await _open();
    return await _db.delete(tableCache);
  }

  Future close() async => _db.close();
}

3、可以看出,非常简单,需求就这么实现了,跑起来没有任何问题,然而如果要考虑的更加全面的化,还是有不少问题的。

还存在的问题

1、LRU策略现在只是简单的做成了缓存100个数据,可以改为动态配置。

2、过期策略似乎还可以优化,比如让数据记录自己有效时间,这样一来,可以更加智能的清理数据,清理过期的,而不是简单除暴的按生成时间去移除。

蓦然回首

当然,我在实现的时候,也了解到有人做了disk_lru_cache了,不过我还是没有使用这个,如果要替换也是相当简单的一件事,不过因为现在这个库测试覆盖不全,评分不是太高,所以暂且还是使用自己的实现。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 首先,我列一下自己的需求
  • 具体的实现代码
  • 还存在的问题
  • 蓦然回首
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档