SharedPreferences灵魂拷问之原理

先来一波灵魂追问:java

  • 据说提交要用apply(),为何?
  • 和commit()什么区别?
  • 跨进程怎么操做?
  • 会堵塞主线程吗?
  • 很着急有替代方案吗?

( 年底福利: 知道你很忙,参考答案可直接看文末... )git

一、加载/初始化

image

一切从getSharedPreference(String name,int Mode)这个方法提及;经过这个方法获取到一个SharedPreference实例。SharedPreferences是一个接口(interface),他的具体实现类为SharedPreferencesImpl。 SharedPreference的加载的主要过程:github

  • 找到对应name的文件。
  • 加载对应文件到内存中SharedPreference。
  • 一个xml文件对应一个ShredPreferences单例
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
private ArrayMap<String, File> mSharedPrefsPaths;
复制代码

sSharedPrefsCache存储的是File和SharedPreferencesImpl键值对,当对应File的SharedPreferencesImpl加载以后就会一支存储于sSharedPrefsCache中。相似的mSharedPrefsPaths存储的是name和File的对应关系。使用的ArrayMap,关于ArrayMap这种Android特有的数据结构,详细了解能够看这juejin.im/post/5d550f…微信

当经过name最终找到对应的File以后,就会实例化一个SharedPreferencesImpl对象。在SharedPreferences构造方法中开启一个子线程加载磁盘中的xml文件。数据结构

你们都应该很明确的一点是,SP持久化的本质是在本地磁盘记录了一个xml文件,这个文件所在的文件夹shared_prefsapp

image

private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }
复制代码

怎么保证使用sp.get(String name)的时候SP的初始化或者说从磁盘中加载到内存中这一过程已经完成了呢?异步

public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

private void awaitLoadedLocked() {
        if (!mLoaded) {
            // Raise an explicit StrictMode onReadFromDisk for this
            // thread, since the real read will be in a different
            // thread and otherwise ignored by StrictMode.
            BlockGuard.getThreadPolicy().onReadFromDisk();
        }
        while (!mLoaded) {
            try {
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
        if (mThrowable != null) {
            throw new IllegalStateException(mThrowable);
        }
    }
复制代码

使用awaitLoadedLocked()方法检测,是否已经加载完成,若是没有加载完成,就等待堵塞。等加载完成以后,继续执行;ide

在loadFromDisk()方法中,若是加载成功会把mLoaded标志位置为true,而后 mLock.notifyAll();post

最终,就把位于磁盘中的文件,加载到了内存中对应一个SharedPreferces对象,SharedPreferences中mMap。ui

二、编辑提交

当想SP中存入数据的时候,实例代码以下。

sharedPreferences.edit().putInt("number", 100).puString("age","18").apply();
sharedPreferences.edit().putInt("number", 100).commit();
复制代码

调用sharedPreferences.edit()返回一个EditorImpl对象,操做数据以后调用apply()或者commit()。

2.一、 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 {}
        notifyListeners(mcr);//通知监听
        return mcr.writeToDiskResult;
      }
      
     //
    private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {
        //若是postWriteRunnable为空表示来自commit()方法调用
        final boolean isFromSyncCommit = (postWriteRunnable == null);
        final Runnable writeToDiskRunnable = new Runnable() {
                @Override
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr, isFromSyncCommit);//写入磁盘
                    }
                    synchronized (mLock) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };
            //当commit提交,且mDiskWritesInFlight为1的时候,直接在当前所在线程执行写入磁盘操做
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                writeToDiskRunnable.run();
                return;
            }
        }
        
        //交个QueuedWork,QueuedWork内部维护了一个HandlerThread,一直执行写入磁盘操做。
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }

复制代码

image

如注释:当调用commit()方法以后

  • 首先将编辑的结果同步到内存中。

  • enqueueDiskWrite()将这个结果同步到磁盘中,enqueueDiskWrite()的第二个参数postWriteRunnable传入空。一般状况下也就是mDiskWritesInFlight(正在执行的写入磁盘操做的数量)为1的时候,直接在当前所在线程执行写入磁盘操做。不然仍是异步到QueuedWork中去执行。commit()时,写入磁盘操做会发生在当前线程的说法是不许确的

  • 执行mcr.writtenToDiskLatch.await(); MemoryCommitResult 中有个一个CountDownLatch 成员变量,他的具体做用能够查阅其余资料。总的来讲,当前线程执行会堵塞在这,直到mcr.writtenToDiskLatch知足了条件。也就是当写入磁盘成功以后,会继续执行下面的操做。

  • 因此,commit提交以后会有返回结果,同步堵塞直到有返回结果

2.二、 apply()流程

@Override
        public void apply() {
            final long startTime = System.currentTimeMillis();
            final MemoryCommitResult mcr = commitToMemory();
            final Runnable awaitCommit = new Runnable() {
                    @Override
                    public void run() {mcr.writtenToDiskLatch.await();}
                   };
            QueuedWork.addFinisher(awaitCommit);
            Runnable postWriteRunnable = new Runnable() {
                    @Override
                    public void run() {
                        awaitCommit.run();
                        QueuedWork.removeFinisher(awaitCommit);
                    }
                };
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
            notifyListeners(mcr);
        }
复制代码
  • 加入到QueuedWork中,是一个单线程的操做。
  • 没有返回结果。
  • 默认会有100ms的延迟

2.3 、QueuedWork

2.3.一、 关于延迟磁盘写入。
/** Delay for delayed runnables, as big as possible but low enough to be barely perceivable */
    private static final long DELAY = 100;
    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);
            } else {
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
复制代码
  • 当apply()方式提交的时候,默认消息会延迟发送100毫秒,避免频繁的磁盘写入操做。
  • 当commit()方式,调用QueuedWork的queue()时,会当即向handler()发送Message。
2.3.二、主线程堵塞ANR

You don't need to worry about Android component lifecycles and their interaction with apply() writing to disk. The framework makes sure in-flight disk writes from apply() complete before switching states.

官方文档中有这样段化话,意思是您不须要担忧Android组件生命周期及其对apply()写入磁盘的影响。框层架确保在切换状态以前完成使用apply()方法正在执行磁盘写入的动做。

然而还真是不让人那么省心。

罪魁祸首在这:

//QueuedWork.java
    public static void waitToFinish() {
        ...
          processPendingWork();//执行文件写入磁盘操做
        ....
    }
    private static void processPendingWork() {
        long startTime = 0;
      ....
     if (work.size() > 0) {
         for (Runnable w : work) {
             w.run();
         }
      ...  
    }
复制代码

waitToFinish()会将,储存在QueuedWork的操做一并处理掉。何时呢?在Activiy的 onPause()、BroadcastReceiver的onReceive()以及Service的onStartCommand()方法以前都会调用waitToFinish()。你们知道这些方法都是执行在主线程中,一旦waitToFinish()执行超时,就会跑出ANR。

至于waitToFinish调用具体时机,查看ActivityThread.java类文件。这里只是说本质原理。

三、跨进程操做的解决方案

\\ContextImpl private void checkMode(int mode) {
        if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {
            if ((mode & MODE_WORLD_READABLE) != 0) {
                throw new SecurityException("MODE_WORLD_READABLE no longer supported");
            }
            if ((mode & MODE_WORLD_WRITEABLE) != 0) {
                throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");
            }
        }
    }
复制代码

Andorid 7.0及以上会抛出异常,Sharepreferences再也不支持多进程模式。多进程共享文件会出现问题的本质在于,由于不一样进程,因此线程同步会失效。要解决这个问题,可尝试跨进程解决方案,如ContentProvider、AIDL、AIDL、Service。

四、替代方案

  • 有问题,主线程堵塞。
  • 效率低。
  • 一不留神容易产生ANR。

既然SharedPreferences有这么多问题?就没人管管吗? 温和的治理方法或者说小建议

4.一、 温和改良派

  • 低频 尽可能保证屡次edit一个apply,缘由上文讲过,尽可能维持低频的写入。
  • 异步 能用apply()方法提交的就用apply()方法提交,缘由这个方法是异步的,有延迟的(100s)
  • 小量 尽可能维持Sharepreferences的体量小些,方便磁盘快速写入。
  • 合规 若是村JSON数据,就不要使用Sharepreferences了,由于SharedPerences本质是xml文件格式存储的,要存储JSON文件须要转义效率很低。不如直接本身编写代码文件读写在App私有目录中存储。

4.二、 激进铲除派

  • 腾讯微信团队的MMKV采用内存映射的方式,解决SharedPreferences的各类问题。
  • 原理基于内存映射mmap,具体使用 原理 源码 github.com/Tencent/MMK…

五、 小结

经过本文咱们了解了SharedPreferences的基本原理。再回头看看文章开头的那几个问题,是否是有答案了。

  • commit()方法和apply()方法的区别:commit()方法是同步的有返回结果,同步保证使用Countdownlatch,即便同步但不保证往磁盘的写入是发生在当前线程的。apply()方法是异步的具体发生在QueuedWork中,里面维护了一个单线程去执行磁盘写入操做。
  • commit()和apply()方法其实都是Block主线程。commit()只要在主线程调用就会堵塞主线程;apply()方法磁盘写入操做虽然是异步的,可是当组件(Activity Service BroadCastReceiver)这些系统组件特定状态转换的时候,会把QueuedWork中未完成的那些磁盘写入操做放在主线程执行,且若是比较耗时会产生ANR,手动可怕。
  • 跨进程操做,须要借助Android平台常规的IPC手段(如,AIDL ContentProvider等)来完成。
  • 替代解决方案:看4。