在多进程中使用 SharedPreference

之前都是使用 SharedPreference 来做一些基本的保存工作,因为都是在同一进程下使用,所以也没有遇到过什么问题,这次偶然间需要在多进程下使用,结果发现在读取时会存在读取不到的问题,因此去看看了源码,找到了问题原因和解决方式,也对 SharedPreference 有了更深的理解,特此记录一下~

获取 SharedPreference

通常我们都是通过 Context.getSharedPreferences() 来获取 SharedPreference 对象,这个 Context 无论是 Application、 Service 或是 Activity,都是继承自 ContextWrapper,通过查看 ContextWrapper 源码可以看发现内部都是调用了 mBase 的相关方法,而这个 mBase 就是 ContextImpl。getSharedPreferences() 在 ContextImpl 有两个重载方法

public SharedPreferences getSharedPreferences(String name, int mode) {
        ...
        synchronized (ContextImpl.class) {
            if (mSharedPrefsPaths == null) {
                mSharedPrefsPaths = new ArrayMap<>();
            }
            file = mSharedPrefsPaths.get(name);
            if (file == null) {
                file = getSharedPreferencesPath(name);
                mSharedPrefsPaths.put(name, file);
            }
        }
        return getSharedPreferences(file, mode);
    }

这个是我们常用的,其中 name 为文件名称,也就是生成后保存在 data 目录下的 xml 文件名称,mode 为操作模式,通常我们传入的都是 Context.MODE_PRIVATE,这里只是去获取 file 文件,然后调用 getSharedPreferences(file, mode) 方法

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) {
                ...
                sp = new SharedPreferencesImpl(file, mode);
                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;
    }

可以看到,在获取 SharedPreference 时,系统会其做一个缓存,因此不会每次都去 新建一个出来,减少了不必要的开销,这里要特别注意最后一段,当 mode 为Context.MODE_MULTI_PROCESS 或是 Android 版本低于 Android 3.0时,会去执行 startReloadIfChangedUnexpectedly() 方法,这个地方就是在多进程下可以使用的原因,后续再说。

get

调用 SharedPreference 的各种 get 方法,其实是从内存中去拿数据,这里以 getString() 为例

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

其中 awaitLoadedLocked() 的作用保证数据已经从文件中加载到内存中,mLoadedloadFromDisk() 中加载完成后,即 Map 被赋值后被置为 true

private void awaitLoadedLocked() {
         ....
        while (!mLoaded) {
            try {
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
}

commit 和 apply

在获取到 SharedPreference 后,要想保存数据,必须要调用 edit() 方法来获取一个 Editor 对象,Editor 是 SharedPreference 内的一个接口,提供了所有的 put、提交以及清除的方法,它的实现是 EditorImpl。
commit() 方法带有一个布尔的返回值,用来返回是否成功将提交写入文件中,而且是同步写入,因此如果要写入的数据过大,会造成线程阻塞,apply() 方法没有返回值,用异步方式写入,也是比较推荐的一种用法。

 public boolean commit() {
        MemoryCommitResult mcr = commitToMemory();
        SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null /* sync write on this thread okay */);
        try {
            //使用 CountDownLatch 来做等待操作
            mcr.writtenToDiskLatch.await();
        } catch (InterruptedException e) {
            return false;
        }
        notifyListeners(mcr);
        return mcr.writeToDiskResult;
}
public void apply() {
        final MemoryCommitResult mcr = commitToMemory();
        ....
        SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
        notifyListeners(mcr);
}

可以看到两个方法中都是先通过调用 commitToMemory() 获取到了一个 MemoryCommitResult 对象,commitToMemory() 主要是将 Editor 中的更改添加到 SharedPreference 的缓存 Map 中去,如果调用了 Clear(),则会去对 Map 做清空操作,接着会遍历更改写入到 Map 中,最后返回一个 MemoryCommitResult 对象

 return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,mapToWriteToDisk);

其中 memoryStateGeneration 是一个长整型,用来记录当前内存的状态,会在每次修改后加一,keysModified 是所有更改的 key 值,listeners 是通过registerOnSharedPreferenceChangeListener() 注册的 Listener 集合,mapToWriteToDisk是修改后需要写入磁盘的 Map。在获取到 MemoryCommitResult 后,会将其传入 SharedPreference 的 enqueueDiskWrite() 方法中

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);
                    }
                    synchronized (mLock) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                writeToDiskRunnable.run();
                return;
            }
        }
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }

这里要注意的是 mDiskWritesInFlight 是在 commitToMemory() 做加一操作,因此,如果传入的 postWriteRunnable 为空,则 wasEmpty 肯定为true,因此 commit() 方法会同步写入,否则会将 postWriteRunnable 传入到 QueuedWork.queue() 中去

LinkedList<Runnable> sWork = new LinkedList<>();
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);
            }
        }
    }

queue () 中会通过 getHandler 来获取到一个 Handler,然后通过这个Handler发送一条消息,其实这里就是 apply() 是异步写入的关键,通过查看 getHandler() 代码,发现里面就是通过 HandlerThread 来获取这个 Handler,因此也完成了线程的切换

 private static Handler getHandler() {
        synchronized (sLock) {
            if (sHandler == null) {
                HandlerThread handlerThread = new HandlerThread("queued-work-looper",
                        Process.THREAD_PRIORITY_FOREGROUND);
                handlerThread.start();
                sHandler = new QueuedWorkHandler(handlerThread.getLooper());
            }
            return sHandler;
        }
    }

Mode

  • Context.MODE_PRIVATE 表明只能被当前应用读写或是分享同一 user ID 的所有应用读写
  • Context.MODE_MULTI_PROCESS 已被标记为弃用,随时可能会被移除,如果在多进程下官方推荐使用 ContentProvider 来进行数据共享

多进程中使用

首先来分析下为什么会出现获取数据为空的情况,之前在看 Context.getSharedPreferences() 时,可以看到 Context 会对 SharedPreference 做一个缓存,即只会在第一次获取时才会新创建对象,因此,对应的 SharedPreference 构造函数中的 startLoadFromDisk() 也只有在第一次才会调用,那么,问题就来了,当你在主进程中添加或修改了数据,而在进程2中已经获取过对应的SharedPreference,这时在进程2中去调用 get 方法,因为进程2内存中保存的 Map 中数据并未更改,所以返回空数据或旧数据。
而当我们把 mode 改为 Context.MODE_MULTI_PROCESS 时为什么就可以获取到正确的数据了呢?主要原因就在 getSharedPreferences() 中,当 mode Context.MODE_MULTI_PROCESS 时,会调用下面这个方法来重新从文件中读取数据

 void startReloadIfChangedUnexpectedly() {
        synchronized (mLock) {
            if (!hasFileChangedUnexpectedly()) {
                return;
            }
            startLoadFromDisk();
        }
}

//还有一处调用是在 SharedPreferencesImpl 的构造函数中
private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
}

因此,这里主要是强制在每次调用 getSharedPreferences() 时都去从文件中重新加载一边,保证此时内存中的数据是最新的。

总结

在源码中可以发现官方已经将 Context.MODE_MULTI_PROCESS 标记为弃用,而且极力推荐使用 ContentProvider 来进行进程间的数据共享,因此使用这个 mode 来在进程下使用 SharedPreference 是不安全的,但有时我们只是需要存储一些简单的数据,用 ContentProvider 好像又有点过于繁琐了,所以我觉得,也需要视情况而定,如果你能保证自己的数据量不大,且使用不是很频繁,那么使用这个 mode 也不失为一个办法。另外,在多进程下想安全的像使用 SharedPreference 来保存和读取数据,不妨试试腾讯开源的 MMKV

    原文作者:wavever
    原文地址: https://www.jianshu.com/p/63ef7f5cf2f3
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞