锦囊篇|一文摸懂SharedPreferences和MMKV(一)

5,456 阅读8分钟

目录

使用方法

SharedPreferences

// 1:获得SharedPreferences,这是直接包含在Context中的方式,直接调用即可
// 四种写入模式:MODE_PRIVATE、MODE_APPEND、MODE_WORLD_READABLE、MODE_WORLD_WRITEABLE
val sp = baseContext.getSharedPreferences("clericyi", Context.MODE_PRIVATE)
// 2:获取笔,因为第一步获得到相当于一张白纸,需要对应的笔才能对其操作
val editor = sp.edit()
// 3:数据操作,不过我们当前操作的数据只是一个副本
// putString()、putInt()。。。还有很多方法
editor.putBoolean("is_wirte", true)
// 4:两种提交方式,将副本内的数据正式写入实体文件中
editor.commit() // 同步写入
editor.apply() // 异步写入

MMKV

第一步:开源库导入

implementation 'com.tencent:mmkv-static:1.1.2'

第二步:使用

// 1. 自定义Aapplication
public void onCreate() {
    super.onCreate();
    MMKV.initialize(this);
}

// 2. 调度使用
// 和SharedPreferenced一样,支持的数据类型直接往里面塞即可
// 不一样的地方,MMKV不需要自己去做一些apply()或者是commit()的操作,更加方便
MMKV kv = MMKV.defaultMMKV();

kv.encode("bool", true);
boolean bValue = kv.decodeBool("bool");

为什么这篇文章要拿两个框架讲?

单进程性能

多进程性能

不论是单线程还是多线程,MMKV的读写能力都远远的甩开了SharedPreferences&SQLite&SQLite+Transacion,但是MMKV到底是如何做到如此快的进行读写操作的?这就是下面会通过源码分析完成的事情了。

另外接下来的一句话仅代表了我的个人意见,也是为什么我只写SharedPreferencesMMKV两者比较的原因,因为我个人认为SQLite和他们不太属于同一类产品,所以比较的意义上来说就趋于普通。

SharedPreferences源码分析

根据上述中所提及过的使用代码,能够比较清楚的知道第一步的分析对象就是getSharedPreferences()的获取操作了,但是如果你直接点进去搜这个方法,是不是会出现这样的结果呢?

public abstract SharedPreferences getSharedPreferences(String name, @PreferencesMode int mode);

没错了,只是一个抽象方法,那显然现在最重要的事情就是找到他的具体实现类是什么了,当然你可以直接查阅资料获取,最后的正确答案就是ContextImpl,不知道你有没有找对呢?

public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);
            if (sp == null) {
                // .....
                // 通过具体实现类,对SharedPreferences进行创建
                sp = new SharedPreferencesImpl(file, mode);
                // 通过一个cache来防止同一个文件的SharedPreferences的重复创建
                cache.put(file, sp);
                return sp;
            }
        }
        if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
            getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
            // 如果开启了多进程模式,一旦数据发生更新,那么其他进程的数据会通过重载的方式更新
            // 这里是否存在疑问,为什么网上会说这个方法是一个进程不安全的方案呢?
            sp.startReloadIfChangedUnexpectedly();
        }
        return sp;
    }

在上面的使用过程中提到了,其他他是一个副本的概念,这个从何说起呢?显然这就要看一下SharedPreferences的实现类具体是如何进行操作的了,从他的构造函数看起,慢慢进入深度调用。

SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = null;
        // 开始从磁盘中调数据
        startLoadFromDisk(); // 1 --> 
    }
// 1 -->
private void startLoadFromDisk() {
        // 开启一条新的线程来加载数据
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk(); // 2-->
            }
        }.start();
    }
// 2 -->
private void loadFromDisk() {
        Map<String, Object> map = null;
        StructStat stat = null;
        Throwable thrown = null;
        // 从XML中把数据读出来,并把数据转化成Map类型
        // 这是一个非常消耗时间的操作
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            str = new BufferedInputStream(
                    new FileInputStream(mFile), 16 * 1024);
            map = (Map<String, Object>) XmlUtils.readMapXml(str);
        }

        synchronized (mLock) {
            mLoaded = true;
            mThrowable = thrown;
            if (thrown == null) {
                // 文件里拿到的数据为空就重建,存在就赋值
                if (map != null) {
                    // 将数据存储放置到具体类的一个全局变量中
                    // 稍微记一下这个关键点
                    mMap = map; 
                    mStatTimestamp = stat.st_mtim;
                    mStatSize = stat.st_size;
                } else {
                    mMap = new HashMap<>();
                }
            }
        }
    }

到此为止就基本完成了SharedPreferences的构建流程,而为了能够对数据进行操作,那就需要去获取一只笔,来进行操作,同样的这段代码最后会在SharedPreferencesImpl中进行具体实现。

public Editor edit() {
        synchronized (mLock) {
            awaitLoadedLocked();
        }
        // 很简单的一个操作,就是创建了一支笔
        // 这里是一个很重要的点,因为每次都是新创建一支笔,所以要做到数据更换的操作要一次性完成。
        return new EditorImpl();
    }

因为后面的操作都是与这只笔相关,而且具体操作上重复度比较高,所以只选取一个putString()来进行分析。

public final class EditorImpl implements Editor {
    private final Map<String, Object> mModified = new HashMap<>();
    
    public Editor putString(String key, @Nullable String value) {
            synchronized (mEditorLock) {
                mModified.put(key, value);
                return this;
            }
        }
}

很简单的解决思路,就是新创建一个了HashMap里面全部保存的都是一些我们已经做过修改的数据,之后的更新是需要用到这些数据的。

相较于之前的那些源码,这里的就显得非常轻松了,结合上述的源码分析,可以假设SharedPreferences氛围三个要点。

  1. mMap: 存储从文件中拉取的数据。
  2. mModified: 存储希望修改值的数据。
  3. apply()/commit(): 猜测最后就是上述两者数据的合并,再进行数据提交。

数据提交

异步提交 / apply()

    public void apply() {
            // .....
            // 这一步其实就是我们所猜测的第三步中的数据合并
            // 做一个简单的介绍,数据的替换一共分为三步:
            // 1. 将数据存储到mapToWriteToDisk中
            // 2. 与mModified中数据进行比较,不存在或者不一致就替换
            // 3. 将更新后得数据返回
            final MemoryCommitResult mcr = commitToMemory();
            // 通过CountDownLatch来完成数据的同步更新
            final Runnable awaitCommit = new Runnable() {
                    @Override
                    public void run() {
                        mcr.writtenToDiskLatch.await(); // 1-->
                    }
                };
            // 对事件完成的监听
            QueuedWork.addFinisher(awaitCommit);

            Runnable postWriteRunnable = new Runnable() {
                    @Override
                    public void run() {
                        // 通过线程进行异步处理
                        awaitCommit.run();
                        // 如果任务完成,就从队列中清除
                        QueuedWork.removeFinisher(awaitCommit);
                    }
                };
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);  // 2 -->
            // 通知观察者数据更新
            notifyListeners(mcr);
        }

从上述的代码中可以了解到apply()的通过创建一个线程来进行处理,之后会讲到commit()和他的处理方式不同的地方。现在具体的目光还是要聚焦在如何完成数据到磁盘的提交的,也就是注释1处的具体实现到底是如何?这就是对这个类的一个理解问题了。其实他有点类似于程序计数器,在阻塞数量大于线程数时,会阻塞运行,而超出数量就会出现并发状况。

第二个地方就是注释2,他线程做了一个入队列的操作。

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        final boolean isFromSyncCommit = (postWriteRunnable == null);

        final Runnable writeToDiskRunnable = new Runnable() {
                @Override
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr, isFromSyncCommit);
                    }
                }
            };
        // commit()同样会进入到这个大方法中
        // commit()方法执行到这里运行完就结束,干的事情就是将数据写入文件
        if (isFromSyncCommit) {
            writeToDiskRunnable.run();
            return;
        }
        // apply()多做了层如队列的操作,意图在于异步进行
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit); // 3-->
    }
// 3 -->
// 因为最后使用的都是其实都是MSG_RUN的参数,所以直接调用查看即可
public static void queue(Runnable work, boolean shouldDelay) {
        Handler handler = getHandler();

        synchronized (sLock) {
            sWork.add(work);

            if (shouldDelay && sCanDelay) {
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY); // 4-->
            } else {
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN); // 4-->
            }
        }
    }
// 4 -->
public void handleMessage(Message msg) {
    if (msg.what == MSG_RUN) {
        processPendingWork(); // 5 -->
    }
}
// 5 -->
// 就是最后将一个个任务进行完成运行
private static void processPendingWork() {
        synchronized (sProcessingWork) {
            LinkedList<Runnable> work;
            // 。。。。。
            if (work.size() > 0) {
                for (Runnable w : work) {
                    w.run(); // 最后将数据一个个进行运行完成操作
                }
            }
        }
    }

同步提交 / commit()

    public boolean commit() {
            
            MemoryCommitResult mcr = commitToMemory();
            // 不需要使用线程来进行异步处理,所以第二参数为空
            SharedPreferencesImpl.this.enqueueDiskWrite(
                mcr, null /* sync write on this thread okay */);
                
            mcr.writtenToDiskLatch.await();
            
            notifyListeners(mcr);
            return mcr.writeToDiskResult;
        }

所以说基本逻辑上其实还是和apply()方法是一致的,只是去除了异步处理的步骤,所以就是常说的同步处理方式。

总结

  1. 是什么限制了SharedPreferences的处理速度?

这个问题在上面的源码分析中其实已经有所提及了,那就是文件读写,所以如何加快文件的读写速度是一个至关重要的突破点。当然向速度妥协的一个方案,想来你也已经看到了,那就是异步提交,通过子线程的在用户无感知的情况下把数据写到文件中。

  1. 为什么多线程安全,而多进程不安全的操作?

多线程安全安全想来是一个非常容易解释的事情了,干一个很简单的事情就是synchronized的加锁操作,对数据的操作进行加锁那势必拿到的最后数据就会是一个安全的数据了。

但是对于多进程呢? 你可能会说在sp.startReloadIfChangedUnexpectedly();这段代码出现的难道不是已经涉及了多进程的安全操作吗?yep!! 如果你想到了这点,说明你有好好看了下代码,但是没有看他的实现,如果你去看他的实现方案,就会发现MODE_MULTI_PROCESS和所可以使用的操作的运算结果均为0,所以在现在的Android版本中这是一个被抛弃的方案。当然这是其一,自然还有另外一个判断就是关于版本方面,如果小于HONEYCOMB同样可以进入这个方案,但是需要注意getSharedPreferences()是只有获取时才会出现的,而SharedPreferences是对于单进程而言的单独实例,数据的备份全部在单个进程完成,所以在进行多进程读写时,发生错误是大概率的。