注册

从SharedPreferences和MMKV看本地数据迁移

1. 前言


之前也有听说过MMKV,但是一直没时间去看,前段时间去简单看看它的相关内容之后觉得挺有意思的,然后就想要不要用MMKV把SP给替换了,这时就又想到了一些数据迁移的问题,所以这次简单谈谈SharedPreferences和MMKV,主要我还是想谈谈数据迁移这个问题。


2. MMKV


腾讯的MMKV,挺牛逼,为什么牛逼,很有想法,这也从侧面体现出想要做出牛逼的东西,你得敢想,然后你想出一套方案之后,还能去实现它。或许你看它的原理你觉得还行,也没多复杂什么的,但你能从0到1的过程想出这个方案然后去实现它吗?


首先要知道它为什么被设计出来,通过官方的介绍:需要一个性能非常高的通用 key-value 存储组件,我们考察了 SharedPreferences、NSUserDefaults、SQLite 等常见组件,发现都没能满足如此苛刻的性能要求。看得出是为了提升性能


那是不是说我觉得MMKV性能比SP好,所以我就用它?并不是这样的,如果你只是用key-value的组件去存状态等少量数据,而且不会频繁的读写,那SP是完全够用的,并且没必要引入MMKV。但是如果你存储的数据大数据复杂,并且频繁读写,假如你这次数据都没写完,又开始写下一次了,那就会有性能上的问题,这时候用MMKV去代替SP完全是一个很好的方案。


因为我当前的项目没有这样的需求,没达到这样的量级,所以暂不需要用到MMKV,但是我简单看了它的原理,比较核心的我觉得就两个思想:mmap和protobuf,其它的append啊这些都是在这基础上进一步优化的操作,核心的就是mmap和protobuf,特别是mmap。所以为什么说牛逼,因为如果是你做,没有参考的情况下,你能想出用mmap这种方案去优化吗?


什么是mmap,内存映射mmap,如果了解过Binder机制,那应该对它多多少少有些印象,如果不知道内存映射是什么,建议可以先去看看Binder机制,了解下一次拷贝的概念,再回来看mmap就知道是什么操作了,就知道为什么它要使用这种思路去做性能提升。


再看看另一个点protobuf,protobuf是一种数据存储格式,它所占用的空间更小,所以也是一个优化的点,占的空间越小,存储时所需要的空间就越小,传送也越快。


2. SharedPreferences


android经常使用的组件,喜欢用它是因为使用起来方便。可以简单看看它是怎么实现的,然后对比一下上面的MMKV。


一般我们调用都是SharedPreferences.Editor的commit()或者apply,然后点进去看发现Editor是一个接口,SharedPreferences也同样是个接口,点它的类看获取它的地方发现在Context里面


public abstract SharedPreferences getSharedPreferences(File file, @PreferencesMode int mode);

看它的子类实现在ContextWrapper里面


@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
return mBase.getSharedPreferences(file, mode);
}

mBase就是Context,点之后又跳到Context里面了,完了,芭比Q了,死循环了,找不到SharedPreferences的实现类了。为什么要讲这个,其实如果你看源码比较多,你就会发现有个习惯,一般具体的实现类都是在抽象接口的后面加Impl,所以我们找SharedPreferencesImpl,当然你还有个办法能找到,就是百度。然后看SharedPreferencesImpl的commit方法


@Override
public boolean commit() {
......

MemoryCommitResult mcr = commitToMemory();

SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
} finally {
if (DEBUG) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " committed after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}

commitToMemory里面只是把数据包装成MemoryCommitResult,然后给enqueueDiskWrite方法


private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
......

final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
......
}
};

......
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

QueuedWork.queue就是放到队列操作,这个就不说的,来看writeToFile(挺长的,我这截取中间一部分)


try {
FileOutputStream str = createFileOutputStream(mFile);

if (DEBUG) {
outputStreamCreateTime = System.currentTimeMillis();
}

if (str == null) {
mcr.setDiskWriteResult(false, false);
return;
}
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);

writeTime = System.currentTimeMillis();

FileUtils.sync(str);

fsyncTime = System.currentTimeMillis();

str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);

if (DEBUG) {
setPermTime = System.currentTimeMillis();
}

try {
final StructStat stat = Os.stat(mFile.getPath());
synchronized (mLock) {
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
}
} catch (ErrnoException e) {
// Do nothing
}

if (DEBUG) {
fstatTime = System.currentTimeMillis();
}

// Writing was successful, delete the backup file if there is one.
mBackupFile.delete();

if (DEBUG) {
deleteTime = System.currentTimeMillis();
}

mDiskStateGeneration = mcr.memoryStateGeneration;

mcr.setDiskWriteResult(true, true);

if (DEBUG) {
Log.d(TAG, "write: " + (existsTime - startTime) + "/"
+ (backupExistsTime - startTime) + "/"
+ (outputStreamCreateTime - startTime) + "/"
+ (writeTime - startTime) + "/"
+ (fsyncTime - startTime) + "/"
+ (setPermTime - startTime) + "/"
+ (fstatTime - startTime) + "/"
+ (deleteTime - startTime));
}

long fsyncDuration = fsyncTime - writeTime;
mSyncTimes.add((int) fsyncDuration);
mNumSync++;

if (DEBUG || mNumSync % 1024 == 0 || fsyncDuration > MAX_FSYNC_DURATION_MILLIS) {
mSyncTimes.log(TAG, "Time required to fsync " + mFile + ": ");
}

return;
} catch (XmlPullParserException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
} catch (IOException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
}

其实能很明显第一眼就看出,是直接用FileOutputStream写到文件中,然后XmlUtils就是把这个文件写成xml的形式。其实SharedPreferences是用xml的格式存储数据相信大家都懂,我这里只是通过代码简单过一遍这个流程。


能看出SharedPreferences和MMKV的不同之处,SP是用FileOutputStream把数据写进本的,而MMKV是用了内存映射,MMKV明显会更快,存储数据的格式方面,SP是用了xml的格式,而MMKV用的是protobuf,明显也是MMKV会更小。


虽然SharedPreferences调用起来方便,但同样的也了一些缺点,比较多进程环境下,比如在某些快速读写的环境中使用apply等。那是不是说我就必须使用MMKV去代替SharedPreferences?其实并不是,你的功能没涉及多进程环境,没涉及频繁大量的读写数据,比如存就只存一个状态,或者说我隔一段时间才读写一次数据量不大的数据,那直接使用SharedPreferences也不会有什么问题。没必要大动干戈,杀鸡还要用牛刀?


3. 数据迁移


这才是我想讲的重点,什么是数据迁移,和SharedPreferences还有MMKV又有什么关系,数据迁移是一个解决问题的思路,和SP还有MMKV是没有关系,只不过我用它们两个来举例会比较好说明。


虽然MMKV好用是吧,假如说你有什么场景,用SP确实无法支持你的业务了,改用MMKV,但是你的旧版本中还是用的SP去存数据,直接覆盖升级可是不会删除磁盘数据的,那你得把SP之前存的xml格式的数据迁移到MMKV中,这就是一个本地数据迁移的过程。


如果从SP迁移到MMKV中,那应该挺简单,我相信MMKV中有对应的方法提供给你,我想腾讯开发的,肯定会考虑到这一点,如果没有,你自己写这个迁移的逻辑也不难。而且SP是android原生提供的组件,所以不会涉及到删除组件之类的操作。但是假如,我说假如,字节也出个key-value的组件,比如叫ByteKV,假如他不是用protobuf,是另一种能把数据压缩更小的格式。这时候你用MMKV,你想去替换成ByteKV,你要怎么做。


有的人就说了,那如果有这种情况,它们也会考虑兼容其它的组件,如果没有,那就在手动写迁移的逻辑,这个又不复杂。手写迁移的逻辑是不复杂,但有没有想过一个问题,你需要去删除之前的库,比如说你之前依赖MMKV,你现在换这个ByteKV之后,你需要不再依赖MMKV ,不然你就会每次换一个新的库,你都重新依赖,并且不删除旧的依赖。


比如你的1.0版本依赖MMKV,2.0版本改用ByteKV,在依赖ByteKV的同时,你还要依赖MMKV吗?SP是没有这个问题,因为它是原生的代码。


我帮你们想了一个办法,假如1.0版本依赖MMKV,我2.0版本当一个过渡版本依赖ByteKV和MMKV,我3.0再把MMKV的依赖去掉行不行?当然不行,那有些用户直接从1.0升到3.0不就导致没迁移的数据没了吗


那这要怎么处理,其实说来也简单,MMKV把数据存到本地的哪个文件这个你知道吧,它用protobuf的方式去存你也知道吧,那这事不就完了,你知道文件存哪里并以什么方式存,那你就能把内容读取出来,这和存的过程已经没有任何关系了。 所以你读这个文件的内容,根本就不需要MMKV,你只需要判断在这个文件夹下有这个文件,并且这个文件是某个格式的,就手动做迁移,迁移完之后再把文件删了。如果你不知道你所用的框架会把数据存到哪里,又是以什么格式存的,那也简单,去看它的源码就知道了。


这里是拿了MMKV来举例,数据库也一样,你改不同的数据库框架,无所谓,你知道它存在哪里,怎么存的,那你不用对应的库也能把数据提出来。


这其实就是数据迁移的原理,我管你是用什么库存的,你的库做的只不过是对存的过程的优化和决定数据的格式。


还有一个要注意的点是,数据不是一次性迁移完的,是部分部分迁移的,你先迁移一部分,然后删除旧文件的那部分数据。


总结


这篇文章其实主要是想简单介绍SP和MMKV的不同,了解MMKV是为何被设计出来,并且站在开发者的一个角度去思考,如果是你,你要怎样才能像他们一样,设计出这样的一套思路。


其次就是关于本地数据迁移的问题,如果去透过现象看本质,我们平时会用到很多别人写的库,为什么用,因为别人写得好,我自己从0开始设计没办法像他们一样设计得这么好,所以使用他们得。但我同样需要知道这其中的原理,知道他们是怎样去实现的。


作者:流浪汉kylin
链接:https://juejin.cn/post/7208844516950065210
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册