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

目录

使用方法

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

第一步:开源库导入java

implementation 'com.tencent:mmkv-static:1.1.2'
复制代码

第二步:使用android

// 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");
复制代码

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

单进程性能安全

多进程性能bash

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

另外接下来的一句话仅表明了个人我的意见,也是为何我只写SharedPreferencesMMKV二者比较的缘由,由于我我的认为SQLite和他们不太属于同一类产品,因此比较的意义上来讲就趋于普通。并发

SharedPreferences源码分析

根据上述中所说起过的使用代码,可以比较清楚的知道第一步的分析对象就是getSharedPreferences()的获取操做了,可是若是你直接点进去搜这个方法,是否是会出现这样的结果呢?app

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中进行具体实现。ide

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是对于单进程而言的单独实例,数据的备份所有在单个进程完成,因此在进行多进程读写时,发生错误是大几率的。

相关文章
相关标签/搜索