专栏首页玩转全栈Flutter中利用MapCache加sqflite实现一个伪LRU三级缓存
原创

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

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

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

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

缓存

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

load缓存

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

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

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

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

具体的实现代码

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

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实现:

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了,不过我还是没有使用这个,如果要替换也是相当简单的一件事,不过因为现在这个库测试覆盖不全,评分不是太高,所以暂且还是使用自己的实现。

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

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

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 移动端性能数搜集及上报系统

    github项目地址https://github.com/bravekingzhang/statis-report-framwork-android

    brzhang
  • 完美级解决web开发跨域问题

    依据我的理解,出于安全原因,浏览器限制从脚本内发起的跨源HTTP请求, 如果你尝试突破这个限制,就是跨域。那么什么情况下会触发跨域呢?

    brzhang
  • 极简配置express+MongoDB

    有时候自己想实现一点玩意,苦于没有后台大佬,自己一个前端狗也搞不定,今天终于不用仍受后台大佬了,自己来做一个后端。

    brzhang
  • 抛开深层次底层,快速入门SpringMVC

    SpringMVC主要有三个核心部分组成,DispatcherServlet、Controller、ViewResolver。      Dispatche...

    Rekent
  • 扩展spring cache 支持缓存多租户及其自动过期

    Spring 支持基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如 EHCache 或者 OSCache),而...

    冷冷
  • 扩展spring cache 支持缓存多租户及其自动过期

    Spring 支持基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如 EHCache 或者 OSCache),而...

    冷冷
  • java web项目中redis集群或单击版配置详解

    IT故事会
  • 什么是ORM?

    一、ORM简介 对象关系映射(Object Relational Mapping,简称ORM)模式是一种为了解决面向对象与关系数据库存在的互不...

    步履不停凡
  • jedis连接单机版和集群版redis

    用户5927264
  • 从头创建您自己的vue.js——第4部分(构建反应性)

    状态反应是当应用程序(一组变量)的状态发生变化时,我们做某事(反应)。我们分两步来完成:

    公众号---人生代码

扫码关注云+社区

领取腾讯云代金券