Hive的封装与实践
前言
之前的文章中我介绍过我的项目在缓存管理中还是使用的 path_provider 插件自行封装的缓存框架【传送门】。
本来是也是无伤大雅属于能用的状态,但是考虑到现在的除了网络请求的缓存,还有应用资源的缓存,导致大量的磁盘IO多少影响到性能,这里才想做出优化使用 Hive 接管应用的缓存。
Hive 也是天然适用于缓存这个场景,并且对比 path_provider 原始的方式有一些一些特点:
a) 数据结构和格式:
Hive使用自定义的二进制格式存储数据,这种格式经过优化,可以快速读写。 直接使用path_provider,你需要自己处理数据的序列化和反序列化。 b) 性能:
Hive针对快速读写进行了优化。它使用内存映射文件(memory-mapped files)来提高性能。 使用path_provider,性能取决于你如何实现文件读写。 c) API和易用性:
Hive提供了高级API,使得数据的存储和检索变得简单。 使用path_provider,你需要自己实现所有的数据管理逻辑。 d) 数据类型支持:
Hive支持多种数据类型,并且可以存储复杂对象。 使用path_provider,你需要自己处理不同数据类型的存储。 e) 加密:
Hive提供了内置的加密支持。 使用path_provider,你需要自己实现加密逻辑。 f) 事务和ACID属性:
Hive支持事务,保证了数据的一致性。 使用path_provider,你需要自己实现这些特性。
那么接下来我就详细的介绍一下并且封装之后进行实践。
一、Hive的介绍与使用
Hive 是一个为 Flutter 和 Dart 设计的轻量级、高性能的 NoSQL 数据库。它是用纯 Dart 编写的,不需要本地依赖,这使得它非常适合 Flutter 应用程序。让我为您详细介绍 Hive 并提供一些使用示例。
1.1 Hive 的主要特点:
- 快速:Hive 的读写操作非常快。
- 跨平台:支持所有 Flutter 平台。
- 类型安全:支持所有 Dart 原始类型、List、Map、DateTime 和 Uint8List。
- 轻量级:小巧简单,易于使用。
- 加密支持:可以加密 box 以保护敏感数据。
- 支持懒加载:可以打开巨大的 box,而不会影响启动时间。
- 支持事务
1.2 安装使用 Hive :
在你的 Flutter 项目的 pubspec.yaml 文件中添加依赖:
dependencies:
hive: ^2.2.3
hive_flutter: ^1.1.0
path_provider: ^2.1.4 # 用于获取存储路径
我们可以已使用 hive_flutter 的快速初始化,也可以自行初始化
final directory = await getApplicationDocumentsDirectory(); // 获取应用的文档目录
Hive.init(directory.path); // 初始化 Hive
或者
void main() async {
await Hive.initFlutter();
runApp(MyApp());
}
使用 Hive 的基本操作:
// 打开一个 box
var box = await Hive.openBox<Person>('personBox');
// 添加数据
var person = Person(name: 'John Doe', age: 30);
await box.add(person);
// 读取数据
var john = box.getAt(0);
print('Name: ${john.name}, Age: ${john.age}');
// 更新数据
john.age = 31;
await john.save();
// 删除数据
await john.delete();
// 关闭 box
await box.close();
使用 Hive 进行 CRUD 操作的更多示例:
// 创建
await box.put('key1', Person(name: 'Alice', age: 25));
// 读取
var alice = box.get('key1');
print('Name: ${alice.name}, Age: ${alice.age}');
// 更新
await box.put('key1', Person(name: 'Alice', age: 26));
// 删除
await box.delete('key1');
// 获取所有键
var keys = box.keys;
// 获取所有值
var values = box.values;
// 使用条件查询
var adults = box.values.where((person) => person.age >= 18);
// 监听变化
box.watch().listen((event) {
print('Box changed: ${event.key}');
});
添加数据注意 add 和 put 的区别:
box.add(person):
这个方法会自动生成一个唯一的键(通常是一个自增的整数)。适用于你不需要指定特定键,只需要按顺序存储对象的情况。返回值是新添加对象的键。这种方法类似于在列表末尾添加一个新项。
box.put('key1', Person(...)):
这个方法允许你指定一个自定义的键(在这个例子中是 'key1')。如果指定的键已经存在,它会覆盖现有的值。适用于你需要使用特定键(如 ID、用户名等)来存储和检索对象的情况。不返回任何值。
当然在我们本地缓存这个场景还是 put 的方法更符合场景。
使用事务:
await box.transaction((txn) async {
await txn.put('key1', Person(name: 'Bob', age: 30));
await txn.put('key2', Person(name: 'Charlie', age: 25));
});
加密 box
var encryptionKey = Hive.generateSecureKey();
var encryptedBox = await Hive.openBox<Person>('secureBox',
encryptionCipher: HiveAesCipher(encryptionKey));
二、Hive的封装与实践
问题来了,我们存储数据是KV的模式,那么是把 key 当做 box 的名称直接 add 一个 value 呢? 还是打开一个指定 name 的box 之后 put 对应的 key 和 value 呢?
再者是我们每次调用都需要打开一个 box 是否方便,是否需要封装使用如何封装?
其实这取决于你的具体需求和数据结构。两种方法都有其适用场景:
a. 把 key 当做 box 的名称,直接 add 一个 value:
优点:简单直接,适合存储单一类型的数据集合。
缺点:可能会创建大量的 box,不利于管理。
适用场景:例如,每个用户一个 box,存储该用户的所有相关数据。
b. 打开一个指定 name 的 box,然后 put 对应的 key 和 value:
优点:更灵活,可以在一个 box 中存储多种类型的数据,便于管理。
缺点:需要自己管理 key 的唯一性。
适用场景:存储应用程序的各种设置,或者同一类型的多个对象。
我个人认为两种都太极端,一个是 Box 太多显著的影响性能,二是 Box 太大也影响读取查询性能。我个人认为应该是按模块分 box ,并且集中管理封装单例 box 对象,例如我们分为 http_cache, app_cache,log_cache 等 固定的 box 名称,用枚举定义,每次打开不同的 Box 之后用单例保存其对象,拿到不同的 box 之后进行对应空间内部的 CURD 则更完美,也更符合”蜂巢“这个概念。
存入:
LocalCacheManager().put("name", "张三");
LocalCacheManager().put("age", 26);
localCache.put("gender", 1);
localCache.put("skills", ["游泳", "跑步", "编程"]);
localCache.put("json", {"code": "200", "msg": "Success", "data": 2});
Log.d("存入缓存成功");
取出:
String? name = await LocalCacheManager().get("name");
int? age = await LocalCacheManager().get("age");
int? gender = await LocalCacheManager().get("gender");
List<String>? skills = await localCache.get("skills");
Map<String, dynamic>? json = await localCache.get("json");
Log.d("获取缓存name:$name");
Log.d("获取缓存age:$age");
Log.d("获取缓存gender:$gender");
Log.d("获取缓存skills:$skills");
Log.d("获取缓存json:$json");
日志:
测试带缓存的存入:
LocalCacheManager().put("name", "李四", expiry: const Duration(seconds: 10));
LocalCacheManager().put("age", 28, expiry: const Duration(seconds: 10));
LocalCacheManager().put("gender", 1);
Log.d("存入缓存成功");
测试缓存的取出
String? name = await LocalCacheManager().get("name");
int? age = await LocalCacheManager().get("age");
int? gender = await LocalCacheManager().get("gender");
Log.d("获取缓存name:$name");
Log.d("获取缓存age:$age");
Log.d("获取缓存gender:$gender");
我们存入之后立马取出,然后等10秒之后再取一次,日志如下:
比较特殊的是对象的存储,例如:
class User{
final String name;
final int age;
User(this.name, this.age);
@override
String toString() {
return 'User{name: $name, age: $age}';
}
}
如果直接存储还是会报错的,因为对象的存储还是需要你自行填写数据适配器并注册到 Hive 中,这里参考 Hive 的文档实现即可:
// 为 User 类注册一个适配器
class UserAdapter extends TypeAdapter<User> {
@override
final typeId = 1; // 确保这个ID是唯一的
@override
User read(BinaryReader reader) {
return User(reader.readString(), reader.readInt());
}
@override
void write(BinaryWriter writer, User obj) {
writer.writeString(obj.name);
writer.writeInt(obj.age);
}
}
// 在初始化时注册适配器
Hive.registerAdapter(UserAdapter());
三、网络请求缓存的实战
得益于我们之前的 Dio 网络请求实战中的封装,我们对于本地缓存的引擎封装只需要替换对应的引擎实现即可。具体可参考我之前的文章【传送门】
我们在 Dio 的缓存拦截器中实现如下:
class CacheControlInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
Map<String, dynamic> headers = options.headers;
final String cacheControlName = headers['cache_control'] ?? "";
//只缓存
if (cacheControlName == CacheControl.onlyCache.name) {
final key = options.uri.toString();
//直接返回缓存
final json = await localCache.get(key);
if (json != null) {
handler.resolve(Response(
statusCode: 200,
data: json,
statusMessage: '获取缓存成功',
requestOptions: RequestOptions(),
));
} else {
handler.resolve(Response(
statusCode: 200,
data: json,
statusMessage: '获取网络缓存数据失败',
requestOptions: RequestOptions(),
));
}
//有缓存用缓存,没缓存用网络请求的数据并存入缓存
} else if (cacheControlName == CacheControl.cacheFirstOrNetworkPut.name) {
final key = options.uri.toString();
final json = await localCache.get(key);
if (json != null) {
handler.resolve(Response(
statusCode: 200,
data: json,
statusMessage: '获取缓存成功',
requestOptions: RequestOptions(),
));
} else {
//处理数据缓存需要的请求头
headers['cache_key'] = key;
options.headers = headers;
//继续转发,走正常的请求
handler.next(options);
}
//用网络请求的数据并存入缓存
} else if (cacheControlName == CacheControl.onlyNetworkPutCache.name) {
final key = options.uri.toString();
//处理数据缓存需要的请求头
headers['cache_key'] = key;
options.headers = headers;
//继续转发,走正常的请求
handler.next(options);
//不满足条件不需要拦截
} else {
handler.next(options);
}
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
if (response.statusCode == 200) {
//成功的时候设置缓存数据放入 headers 中
//响应体中请求体的请求头数据
final Map<String, dynamic> requestHeaders = response.requestOptions.headers;
if (requestHeaders['cache_control'] != null) {
final cacheKey = requestHeaders['cache_key'];
final cacheControlName = requestHeaders['cache_control'];
final cacheExpiration = requestHeaders['cache_expiration'];
//网络请求完成之后获取正常的Json-Map
Map<String, dynamic> jsonMap = response.data;
Log.d('response 中携带缓存处理逻辑 cacheControl ==== > $cacheControlName '
'cacheKey ==== > $cacheKey cacheExpiration ==== > $cacheExpiration');
Duration? duration;
if (cacheExpiration != null) {
duration = Duration(milliseconds: int.parse(cacheExpiration));
}
//直接存入Json数据到本地File
localCache.put(
cacheKey ?? 'unknow',
jsonMap,
expiry: duration,
);
}
}
super.onResponse(response, handler);
}
@override
Future onError(DioException err, ErrorInterceptorHandler handler) async {
super.onError(err, handler);
}
}
把存入缓存和取出缓存的换为我们的 localCache 对象即完成修改,注意这里的网络请求我们默认是存入的 app_cache 的 box 中,如果有需求可以自定义容器或者指定容器。
localCache.put(
cacheKey ?? 'unknow',
jsonMap,
boxType: CacheBoxType.httpCache,
expiry: duration,
);
final json = await localCache.get(key, boxType: CacheBoxType.httpCache);
使用的时候我们只需要开启缓存。
//Get请求
final result = await httpProvider.requestNetResult(
ApiConstants.apiServerTime,
isShowLoadingDialog: true,
cacheControl: CacheControl.cacheFirstOrNetworkPut,
cacheExpiration: Duration(seconds: 10),
);
结果:
总结
本文介绍了 Hive 的简单使用,以及对应的封装和应用场景的示例,可以看到是可以完全平替 path_provider 的缓存框架的。
总的来说,Hive是一个更高级的解决方案,它在 path_provider 的基础上提供了许多额外的功能和优化。虽然两者都涉及磁盘IO,但Hive提供了更多的抽象和优化,并且内部有内存优化使得数据存储和检索更加快速高效和方便。
在实际开发中,如果你的数据存储需求简单,使用 path_provider 直接操作文件可能足够。但如果你需要一个高效、可扩展、易于管理的解决方案,Hive 是一个更好的选择。
如果对详细的代码有兴趣,也可以参考我的开源项目 【Flutter Room】。
那么今天的分享就到这里啦,当然如果你有其他的更多的更好的实现方式,也希望大家能评论区交流一起学习进步。如果我的文章有错别字,不通顺的,或者代码、注释、有错漏的地方,同学们都可以指出修正。
如果感觉本文对你有一点的启发和帮助,还望你能点赞支持一下,你的支持对我真的很重要。
Ok,这一期就此完结。
