在做flutter应用的时候,遇到了一个问题,纯粹属于自己给自己加戏,问题是什么呢?我的app首页是一个列表,目前每次进应用,都是通过网络拿到新的列表,所以,如果没有网络了,就看到了一个菊花,这样的用户体验可能并不怎么好吧,因此,这块的化,想给自己挖一个坑,让自己填一下,本来以为是一个非常简单的问题,因为如果是在Android平台上,用DiskLruCache,很容易就实现了这个需求啦。然而不信的是,经过我的调研,flutter仓库中的库不太符合要求。
1、网络请求,我使用的是dio框架,在其上面稍微封装了一下,我的想法是需要在onSuccess回调中把get请求缓存下来,就像下面这样:
2、然后,在需要的地方,我需要判断缓存是否可用,如果可用,我就直接返回了,不发起网络请求,或者说,返回,并且发起网络请求,这依赖于业务需求,先不说这么多,大概方式是:
其中红框中的就是我通过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了,不过我还是没有使用这个,如果要替换也是相当简单的一件事,不过因为现在这个库测试覆盖不全,评分不是太高,所以暂且还是使用自己的实现。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。