KV存储无论对于客户端还是服务端都是重要的构件。
对于Android客户端而言,最常见的莫过于SDK提供的SharePreferences(以下简称**SP**),但其低效率和ANR问题饱受诟病。
18年年末微信开源了MMKV, 写入速度前者快很多。
后来Android官方又推出了基于Kotlin的DataStore, 测试了一下,发现写入很慢。
我之前写过一个叫LightKV的存储组件,当时认知不足,设计不够成熟。
关于SP的缺点网上有不少讨论,这里主要提两个点:
public void apply() {
// ...省略无关代码...
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
}
public void handleStopActivity(IBinder token, boolean show, int configChanges,
PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {
// ...省略无关代码...
// Make sure any pending writes are now committed.
if (!r.isPreHoneycomb()) {
QueuedWork.waitToFinish();
}
}
Activity stop时会等待SP的写入任务,如果SP的写入任务多且执行慢的话,可能会阻塞主线程较长时间,轻则卡顿,重则ANR。
虽然说现在APP体积都不小,但毕竟增加体积对打包、分发和安装时间都多少有些影响。
这个表述对一半不对一半。
如果数据完成写入到内存块,如果系统不崩溃,即使进程崩溃,系统也会将buffer刷入磁盘;
但是如果在刷入磁盘之前发生系统崩溃或者断电等,数据就丢失了,不过这种情况发生的概率不大;
另一种情况是数据写一半的时候进程崩溃或者被杀死,然后系统会将已写入的部分刷入磁盘,再次打开时文件可能就不完整了。
例如,MMKV在剩余空间不足时会回收无效的空间,如果这期间进程中断,数据可能会不完整。
MMKV官方的说明可以佐证:
CRC校验失败之后,MMKV有两种应对策略:直接丢弃所有数据,或者尝试读取数据(用户可以在初始化时设定)。
尝试读取数据不一定能恢复数据,甚至可能会读到一些错误的数据,得看运气。
这个过程是比较容易复现的,下面是其中一种复现路径:
相比之下,SP虽然低效,但至少不会丢失数据。
在总结了之前的经验和感悟之后,笔者实现了一个高效且可靠的版本,且将其命名为: FastKV。
FastKV有以下特性:
文件的布局:
data_len | checksum | key-value | key-value|....
key-value的数据布局:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| delete_flag | external_flag | type | key_len | key_content | value |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 1bit | 1bit | 6bits | 1 byte | | |
dependencies {
implementation 'io.github.billywei01:fastkv:1.0.2'
}
FastKVConfig.setLogger(FastKVLogger)
FastKVConfig.setExecutor(ChannelExecutorService(4))
初始化可以按需设置日志回调和Executor。
建议传入自己的线程池,以复用线程。
日志接口提供三个级别的回调,按需实现即可。
public interface Logger {
void i(String name, String message);
void w(String name, Exception e);
void e(String name, Exception e);
}
FastKV kv = new FastKV.Builder(path, name).build();
if(!kv.getBoolean("flag")){
kv.putBoolean("flag" , true);
} FastKV.Encoder<?>[] encoders = new FastKV.Encoder[]{LongListEncoder.INSTANCE};
FastKV kv = new FastKV.Builder(path, name).encoder(encoders).build();
String objectKey = "long\_list";
List<Long> list = new ArrayList<>();
list.add(100L);
list.add(200L);
list.add(300L);
kv.putObject(objectKey, list, LongListEncoder.INSTANCE);
List<Long> list2 = kv.getObject("long\_list");
除了支持基本类型外,FastKV还会支持写入对象,只需在构建FastKV实例时传入对象的编码器即可。
编码器为实现FastKV.Encoder的对象。
比如上面的LongListEncoder的实现如下:
public class LongListEncoder implements FastKV.Encoder<List<Long>> {
public static final LongListEncoder INSTANCE = new LongListEncoder();
@Override
public String tag() {
return "LongList";
}
@Override
public byte[] encode(List<Long> obj) {
return new PackEncoder().putLongList(0, obj).getBytes();
}
@Override
public List<Long> decode(byte[] bytes, int offset, int length) {
PackDecoder decoder = PackDecoder.newInstance(bytes, offset, length);
List<Long> list = decoder.getLongList(0);
decoder.recycle();
return (list != null) ? list : new ArrayList<>();
}
}
编码对象涉及序列化/反序列化。
这里推荐笔者的另外一个框架:https://github.com/BillyWei01/Packable
Android平台上的用法和常规用法一致,不过Android平台多了SharePreferences API,以及支持Kotlin。
FastKV的API兼容SharePreferences, 可以很轻松地迁移SharePreferences的数据到FastKV。
相关用法可参考:https://github.com/BillyWei01/FastKV/blob/main/android_case_CN.md
测试结果:
写入(ms) | 读取(ms) | |
---|---|---|
SharePreferences | 1258 | 3 |
DataStore | 16650 | 3 |
MMKV | 25 | 9 |
FastKV | 16 | 1 |
本文探讨了当下Android平台的各类KV存储方式,提出并实现了一种新的存储组件,着重解决了KV存储的效率和数据可靠性问题。
目前代码已上传Github: https://github.com/BillyWei01/FastKV
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。