Flutter Hive Box 基本使用和源码分析(数据结构)

2,546 阅读6分钟

hive box 简介

hive box是一个轻量级key-value数据库,其优秀的性能和数据可监听特性,对于大小型项目用来缓存数据非常有用。

官方给了一个对比图,大家可以大致看看

image.png

基本使用

  1. 初始化
await Hive.initFlutter();
  1. 打开box
var box = await Hive.openBox('testBox');
  1. 写数据
box.put('name', 'David');
  1. 读数据
print('Name: ${box.get('name')}');

还有存一些自定义类型,监听数据变化等特性,使用起来也很简单。可以参考官方文档 docs.hivedb.dev

从上面的使用文档可以看出来Flutter项目使用hive box储存数据很便利,但我们想知道他是如何把数据加载到内存,写到磁盘,数据结构和数据格式又是什么样的。我们有只要从其源码入手,一步步分析。有兴趣的读者可以跟着流程图结合源码一块看,当然也可以直接看每一个流程图后面的结论。也可以大致明白。

写数据put(key, value)

写数据流程图如下

UML diagram.jpg

写入数据的格式如下

UML diagram (1).jpg

通过查看hive box文件进一步分析

  1. Key

0104 01代表key的类型是字符串 04代表key的长度

6E616D65 代表key的utf-8编码内容, 解码之后可以得到 name

  1. Value:

04050000 00446176 6964

04代表value的类型,字符串

05 代表value的长度 5

446176 6964 代表value的utf-8编码内容,解码之后可以得到David

  1. 相同的key, 会写两次

  1. key的长度一个字节,

说明hive box的key长度不能能超过255

  1. value类型长度一个字节

说明hive box数量不能超过255个

打开 openBox、opanLazyBox

UML diagram (2).jpg

细节解析
  1. 通过流程图可以发现Box和LazyBox打开方式基本上相同,唯一不同的的是LazyBox在打开的时候没有读取Value数据。
  1. 前面发现hive box的磁盘储存数据中发现,相同的key会在磁盘中写多次,当然后面加入的key追加的流的后面,这样在打开加载数据到内存中的时候,二进制数据流后面key的value会覆盖内存中已存在key的value
  1. 对于已删除的key, 其实也会写到磁盘,只不过value长度为0,这样在读取的时候会通过Frame.delete()构造一个标记已删除的实例。

读 box.get(key)

UML diagram (3).jpg

细节解析

对于box.get(key),直接从内存中读取缓存的数据

对于lazybox.get(key), 每次都是通过frame中的offset和length加载储存二进制流中指定片段的数据块Data, 然后创建一个解析数据对象来对这个Data进行解析。 这个地方有一个细节就是lazybox不会更新已经拿出来的数据,每次get相同的key都会重新加载,重新解析。

删除key,box.delete(key)

UML diagram (4).jpg

细节解析

如果仔细看过了往box添加数据的流程,那删除流程其实比较简单了。 删除其实除了value没有写,其他的数据都写了。

通过流程图可以发现删除同box.put(), 只不过是写一个只有key,且value为空的内容,下面的截图可以直观的发现删除的key=name加到了文件末尾,且没有内容。

box压缩compact

UML diagram (5).jpg

细节解析
  • 通过流程图可以发现compact()主要作用是删除了文件中的重复的key, 已删除的key

UML 图.jpg

  • 当然在压缩的时候,还有一些细节在流程图上没有体现,比如对于buffer剩余长度, 当次可读取的长度,frame总长度,通过这些长度进行数据校验,数据异常就直接丢弃。
  • 可见这个box压缩对于会有很大重复key的box, 经常删除一些key,非常有用。
  • 默认压缩策略

hive在打开box的时候,也提供了默认压缩策略,代码如下,其实参数是entries是box数据总量, deletedEntries顾名思义就是当前删除的key数量。进入压缩条件就是删除的数量大于60,并且 删除数量除总量比例大于0.15。

const _deletedRatio = 0.15;
const _deletedThreshold = 60;

/// Default compaction strategy compacts if 15% of total values and at least 60
/// values have been deleted
bool defaultCompactionStrategy(int entries, int deletedEntries) {
  return deletedEntries > _deletedThreshold &&
      deletedEntries / entries > _deletedRatio;
}

那时候什么时候触发这个回调呢? 通过源码可以很容易发现是box.put()方法和box.delete()方法中

整体分析

  • 在整个流程中,有几个地方可以发现hive的整体思想就是用空间换时间。比如添加,删除key,并不是真实意义上的添加,删除相应的数据,而是直接在数据流中追加新数据,这样的话,的确容易导致hive box做了一些无效的储存而导致box变大,但是这样时效会更好。
  • box在写入和读取的时候都是在crc32校验,对于校验异常数据,直接放弃操作而往后解析写或者读,这样导致的异常数据还是占据着储存空间,当然下次box压缩之后这些异常数据便会真实丢弃。但是默认提供的压缩方法,其实很紧,比较难触发。需要使用者再加一些调用压缩的时机。
  • 分析发现其实openbox的时候,做了不少的事情,读取帧长,读取key, crc32校验,读取value,对于自定义对象,读取value还是进一步转换,这样解析下来,对于数据量太大的box, 打开时间还是难以忍受。这个地方,最好还是hive box本身可以新增一个数据量大于某一个阈值(比如作者自己说的5000条)进行数据淘汰策略。通过前面的分析也好实现,优先删除文件二进制流前面的数据就行。
  • 文字多次提及的IndexableSkipList数组,hive box每条缓存数据在内存中储存的结构。他其实是一个跳跃表,跳跃表是单链表的升级,主要作用其实是为了快速查询,把通过key查value的数组遍历时间复杂度由O(n)降低到O(log n),这样openBox的时候,可以更快的更新相同key的value。** 具体实现细节可参考
  • hive支持js, 其实在js中缓存使用的indexedDB, 这个就类似sqlite, 没什么可说的。