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开端规划没方法像他们一样规划得这么好,所以运用他们得。但我相同需求知道这其间的原理,知道他们是怎样去完成的。

本文正在参与「金石方案」