SharedPreferences 源码解析及应用(多进程解决方案)

偶然看到一个问题,SharedPreferences 是线程安全的吗?SharedPreferences 是进程安全的吗?如果不是,那如何保证线程安全、进程安全呢?

SharedPreferences 经常用,也知道不是进程安全的,但这都是从书里看到的,并没有深入去研究过,这次从源码角度来分析一下以上这几个问题。

一、SharedPreferences 是线程安全的吗?SharedPreferences 是进程安全的吗?

  • SharedPreferences 是线程安全的,因为内部有大量 synchronized 关键字保障。
  • SharedPreferences 不是进程安全的,因为首次是从磁盘读取,之后都是从内存读取。

二、SharedPreferences 源码解析

1、SharedPreferences 使用

SharedPreferences 的使用分为保存数据和读取数据。

每个 SharedPreferences 都对应了当前 package 的 data/data/package_name/share_prefs/ 目录下的一个 xml 文件。保存数据和读取数据其实就是写入和读取 xml 文件。

保存数据步骤:

  • 获取 SharedPreferences 对象
  • 通过 Editor 获取编辑器对象
  • 以键值对的形式写入数据
  • 提交修改
// 1、获取 SharedPreferences 对象,有两种方式
// 方式一
// 参数1:指定该文件的名称,参数2:指定文件的操作模式,共有 4 种操作模式,分别是:
// Context.MODE_PRIVATE = 0:为默认操作模式,代表该文件是私有数据,只能被应用本身访问,在该模式下,写入的内容会覆盖原文件的内容
// Context.MODE_APPEND = 32768:该模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件。
// Context.MODE_WORLD_READABLE = 1:表示当前文件可以被其他应用读取
// Context.MODE_WORLD_WRITEABLE = 2:表示当前文件可以被其他应用写入
SharedPreferences sharedPreferences = context.getSharedPreferences("trampcr_sp", Context.MODE_PRIVATE);
// 方式二
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);

// 2、获取编辑器对象
Editor editor = sharedPreferences.edit();

// 3、以键值对的方式写入数据
editor.putString("name", "trampcr");
editor.putString("rank", "T6");

// 4、提交修改,有两种方式
// 方式一
editor.apply();
// 方式二
editor.commit();

读取数据步骤:

  • 获取 SharedPreferences 对象
  • 通过 SharedPreferences 对象读取之前保存的值
// 1、获取 SharedPreferences 对象,有两种形式,上边已经写过了,这里只写一种形式
SharedPreferences sharedPreferences = getSharedPreferences("ljq", Context.MODE_PRIVATE);

// 2、通过 SharedPreferences 对象 的 getXxx() 方法读取之前保存的值(Xxx 为数据类型)
String name = sharedPreferences.getString("name", "");
String age = sharedPreferences.getInt("rank", "");

2、SharedPreferences 源码解析

(1)SharedPreferences 对象的获取,有两种方式:
  • PreferenceManager.getDefaultSharedPreferences()
  • ContextImpl.getSharedPreferences()
// PreferenceManager.java
public static SharedPreferences getDefaultSharedPreferences(Context context) {
    return context.getSharedPreferences(getDefaultSharedPreferencesName(context), getDefaultSharedPreferencesMode());
}

// Context.java
public abstract SharedPreferences getSharedPreferences(String name, @PreferencesMode int mode);

// ContextWrapper.java
public SharedPreferences getSharedPreferences(String name, int mode) {
    return mBase.getSharedPreferences(name, mode);
}

跟到这里发现调用了 mBase.getSharedPreferences(name, mode) 方法,那这个 mBase 究竟是什么东西呢?

搜了一下 mBase 定义和赋值:

// ContextWrapper.java
// Context mBase;
protected void attachBaseContext(Context base) {
    if (mBase != null) {
        throw new IllegalStateException("Base context already set");
    }
    mBase = base;
}

从以上可以看出,mBase 是一个 Context 对象,在 attachBaseContext() 方法中进行赋值,这个方法有点面熟,好像在 ActivityThread 创建 Activity 那里见过类似的创建上下文的代码,去 ActivityThread.handleLaunchActivity() 去看看。

// ActivityThread.java 无关代码都先删了
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
    ...

    Activity a = performLaunchActivity(r, customIntent);

    ...
}

// ActivityThread.java
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    ...

    ContextImpl appContext = createBaseContextForActivity(r);
    Activity activity = null;
    try {
        java.lang.ClassLoader cl = appContext.getClassLoader();
        activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
        ...
    } catch (Exception e) {
        ...
    }

    try {
        Application app = r.packageInfo.makeApplication(false, mInstrumentation);

        if (activity != null) {
            ...
                
            appContext.setOuterContext(activity);
            activity.attach(appContext, this, getInstrumentation(), r.token,
                    r.ident, app, r.intent, r.activityInfo, title, r.parent,
                    r.embeddedID, r.lastNonConfigurationInstances, config,
                    r.referrer, r.voiceInteractor, window, r.configCallback);
            ...
        }
        ...
    } 
    ...

    return activity;
}

// ActivityThread.java
private ContextImpl createBaseContextForActivity(ActivityClientRecord r) {
    ...

    ContextImpl appContext = ContextImpl.createActivityContext(this, r.packageInfo, r.activityInfo, r.token, displayId, r.overrideConfig);
    ...
    
    return appContext;
}

// ActivityThread.java
static ContextImpl createActivityContext(ActivityThread mainThread, LoadedApk packageInfo, ActivityInfo activityInfo, 
    IBinder activityToken, int displayId, Configuration overrideConfiguration) {
    ...

    ContextImpl context = new ContextImpl(null, mainThread, packageInfo, activityInfo.splitName, activityToken, null, 0, classLoader);
    ...
    
    return context;
}

这里先简单总结一下上边的内容,再往后分析。

ActivityThread.handleLaunchActivity() 
-> performLaunchActivity() 
-> createBaseContextForActivity() 
-> createActivityContext()
-> 回到 performLaunchActivity() 调用 activity.attach(appContext,...)

以上过程可以简单的理解为创建一个 ContextImpl 对象,然后将该 ContextImpl 对象传入 Activity.attach() 方法。

// Activity.java
final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, 
    int ident, Application application, Intent intent, ActivityInfo info, CharSequence title, 
    Activity parent, String id, NonConfigurationInstances lastNonConfigurationInstances,
    Configuration config, String referrer, IVoiceInteractor voiceInteractor,
    Window window, ActivityConfigCallback activityConfigCallback) {
    // 果然调用了 attachBaseContext()
    attachBaseContext(context);
    ...
}

// ContextThemeWrapper.java
protected void attachBaseContext(Context newBase) {
    super.attachBaseContext(newBase);
}

// ContextWrapper.java
protected void attachBaseContext(Context base) {
    if (mBase != null) {
        throw new IllegalStateException("Base context already set");
    }
    mBase = base;
}

看来之前的猜测是正确的,果然是从 ActivityThread 中创建了 ContextImpl 对象,并赋值给 mBase,所以 mBase.getSharedPreferences() 就是 ContextImpl.getSharedPreferences()。

这正是获取 SharedPreferences 的第二种方式,所以,第一种方式 PreferenceManager.getDefaultSharedPreferences() 其实就是对第二种方式 ContextImpl.getSharedPreferences() 的封装,最终实现都在 ContextImpl.getSharedPreferences() 中。

// ContextImpl.java
public SharedPreferences getSharedPreferences(File file, int mode) {
    ...
    
    SharedPreferencesImpl sp;
    
    // 创建 SharedPreferences 对象使用 synchronized 关键字修饰,所以创建实例过程是线程安全的
    synchronized (ContextImpl.class) {
        // 先从缓存中获取 SharedPreferences 对象
        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;
}

// ContextImpl.java
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
    if (sSharedPrefsCache == null) {
        sSharedPrefsCache = new ArrayMap<>();
    }

    final String packageName = getPackageName();
    ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
    if (packagePrefs == null) {
        packagePrefs = new ArrayMap<>();
        sSharedPrefsCache.put(packageName, packagePrefs);
    }

    return packagePrefs;
}

由上可知:获取 SharedPreferences 对象先从缓存中获取,如果缓存中没有,则创建;同时,实例的创建是被 synchronized 修饰的,所以创建 SharedPreferences 对象的过程是线程安全的。

接下来看下 SharedPreferences 对象的创建:

// SharedPreferencesImpl.java
SharedPreferencesImpl(File file, int mode) {
    mFile = file; 
    mBackupFile = makeBackupFile(file); // 备份文件,用于写入失败时进行恢复
    mMode = mode;
    mLoaded = false;
    mMap = null; // 在内存中缓存的数据集合, 也就是 getXxx() 数据的来源
    startLoadFromDisk();
}

// SharedPreferencesImpl.java
private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    
    // 开启一个线程从磁盘文件读取数据
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}

// SharedPreferencesImpl.java
private void loadFromDisk() {
    synchronized (mLock) {
        if (mLoaded) {
            return;
        }
        // 1、如果有备份文件,则直接使用备份文件(重命名)
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }

    ...
    
    try {
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            ...
            try {
                // 2、从磁盘读取文件到内存
                str = new BufferedInputStream(new FileInputStream(mFile), 16*1024);
                map = XmlUtils.readMapXml(str);
            } 
            
            ...
        }
    } catch (ErrnoException e) {
    }

    synchronized (mLock) {
        // 3、标记读取完成,这个字段后面 awaitLoadedLocked() 方法会用到
        mLoaded = true;
        if (map != null) {
            // 4、将从磁盘读取到的文件内容保存在 mMap 字段中
            mMap = map;
            
            // 5、记录读取文件时间,后面 MODE_MULTI_PROCESS 中会用到
            mStatTimestamp = stat.st_mtime;
            mStatSize = stat.st_size;
        } else {
            mMap = new HashMap<>();
        }
        
        // 6、发一个 notifyAll() 通知已经读取完毕,唤醒所有等待加载的其他线程
        mLock.notifyAll();
    }
}

SharedPreferencesImpl 构造方法中调用 startLoadFromDisk(),在 startLoadFromDisk() 中开启了一个线程 调用 loadFromDisk() 从磁盘文件读取数据,它做了如下几件事:

  1. 如果有备份文件,则直接使用备份文件(重命名)
  2. 从磁盘读取文件到内存
  3. 标记读取完成(mLoaded = true;),这个字段后面 awaitLoadedLocked() 方法中会用到
  4. 将从磁盘读取到的文件内容保存在 mMap 字段中
  5. 记录读取文件的时间(mStatTimestamp = stat.st_mtime;),后面 MODE_MULTI_PROCESS 中会用到
  6. 发一个 notifyAll() 通知已经读取完毕,唤醒所有等待加载的其他线程
(2)获取编辑器对象
Editor editor = sharedPreferences.edit();

// SharedPreferencesImpl.java
public Editor edit() {
    synchronized (mLock) {
        awaitLoadedLocked();
    }

    return new EditorImpl();
}

// SharedPreferencesImpl.java
private void awaitLoadedLocked() {
    if (!mLoaded) {
        BlockGuard.getThreadPolicy().onReadFromDisk();
    }
    
    while (!mLoaded) {
        try {
            // 当没有读取完配置文件,先等待,此时不能返回 Editor,也不能保存数据
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
}

在 new EditorImpl() 之前,先调用 awaitLoadedLocked(),如果 mLoaded = false,即没有读取完配置文件时,会卡在这里,直到 SharedPreferencesImpl.loadFromDisk() 读取完毕后调用 notifyAll() 通知所有等待的线程才会返回 EditorImpl 对象。

EditorImpl 是 SharedPreferencesImpl 的一个内部类,没有构造方法,只有两个属性被初始化。

// SharedPreferencesImpl.java
public final class EditorImpl implements Editor {
    private final Object mLock = new Object();

    @GuardedBy("mLock")
    // 保存 putXxx 传入数据的集合
    private final Map<String, Object> mModified = Maps.newHashMap();

    @GuardedBy("mLock")
    private boolean mClear = false;
    
    ...
}
(3)以键值对的形式写入数据
editor.putString("name", "trampcr");
editor.putString("rank", "T6");

// SharedPreferencesImpl.EditorImpl.java
public Editor putString(String key, @Nullable String value) {
    synchronized (mLock) {
        // 保存键值对到 mModified 中
        mModified.put(key, value);
        return this;
    }
}
(4)提交修改

有两种方式:

  • editor.apply();
  • editor.commit();
// SharedPreferencesImpl.EditorImpl.java
public void apply() {
    final long startTime = System.currentTimeMillis();
    
    // 1、把以键值对写入的数据保存到内存
    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
        public void run() {
            try {
                mcr.writtenToDiskLatch.await();
            } catch (InterruptedException ignored) {
            }
        }
    };

    QueuedWork.addFinisher(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
        public void run() {
            awaitCommit.run();
            QueuedWork.removeFinisher(awaitCommit);
        }
    };

    // 2、把保存到内存的数据加入到一个异步队列中, 等待调度
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

    notifyListeners(mcr);
}

// SharedPreferencesImpl.java
private MemoryCommitResult commitToMemory() {
    long memoryStateGeneration;
    List<String> keysModified = null;
    Set<OnSharedPreferenceChangeListener> listeners = null;
    Map<String, Object> mapToWriteToDisk;

    synchronized (SharedPreferencesImpl.this.mLock) {
        if (mDiskWritesInFlight > 0) {
            mMap = new HashMap<String, Object>(mMap);
        }
        
        // 将 SharedPreferences.mMap 保存在 mcr.mapToWriteToDisk 中,mcr.mapToWriteToDisk 稍后会被写到磁盘
        mapToWriteToDisk = mMap;
        mDiskWritesInFlight++;

        ...

        synchronized (mLock) {
            ...

            for (Map.Entry<String, Object> e : mModified.entrySet()) {
                String k = e.getKey();
                Object v = e.getValue();
                
                if (v == this || v == null) {
                    if (!mMap.containsKey(k)) {
                        continue;
                    }
                    mMap.remove(k);
                } else {
                    if (mMap.containsKey(k)) {
                        Object existingValue = mMap.get(k);
                        if (existingValue != null && existingValue.equals(v)) {
                            continue;
                        }
                    }
                    
                    // putXxx 方法保存键值对到 Editor.mModified 中,这里把 mModified 中的数据写到 SharedPreferences.mMap 中, 这一步完成了内存的同步
                    mMap.put(k, v);
                }

                changesMade = true;
                if (hasListeners) {
                    keysModified.add(k);
                }
            }

            mModified.clear();

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

// SharedPreferencesImpl.java
private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
    final Runnable writeToDiskRunnable = new Runnable() {
        public void run() {
            synchronized (mWritingToDiskLock) {
                writeToFile(mcr, isFromSyncCommit);
            }
            ...
        }
    };

    ...

    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

// SharedPreferencesImpl.java
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
    ...

    boolean fileExists = mFile.exists();

    ...

    if (fileExists) {
        boolean needsWrite = false;

        // 只有当磁盘的状态比目前的提交状态老的时候才写磁盘
        if (mDiskStateGeneration < mcr.memoryStateGeneration) {
            if (isFromSyncCommit) {
                needsWrite = true;
            } else {
                synchronized (mLock) {
                    if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                        needsWrite = true;
                    }
                }
            }
        }
        
        ...

        boolean backupFileExists = mBackupFile.exists();

        if (!backupFileExists) {
            // 把已经存在的老文件重命名(加 .bak 后缀)为备份文件
            if (!mFile.renameTo(mBackupFile)) {
                mcr.setDiskWriteResult(false, false);
                return;
            }
        } else {
            // 如果已经有了备份文件,则删除老的配置文件
            mFile.delete();
        }
    }

    try {
        FileOutputStream str = createFileOutputStream(mFile);

        if (str == null) {
            mcr.setDiskWriteResult(false, false);
            return;
        }
        
        // 将保存在 mcr.mapToWriteToDisk 中的所有键值对写入 mFile 中
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);

        writeTime = System.currentTimeMillis();
        
        ...

        try {
            final StructStat stat = Os.stat(mFile.getPath());
            synchronized (mLock) {
                // 记录写入到磁盘的时间
                mStatTimestamp = stat.st_mtime;
                mStatSize = stat.st_size;
            }
        } catch (ErrnoException e) {
            // Do nothing
        }

        // 如果写入磁盘成功则删除备份文件
        mBackupFile.delete();

        ...

        return;
    } 
    
    ...
    
    if (mFile.exists()) {
        // 如果写入磁盘失败, 则删除这个文件
        if (!mFile.delete()) {
            Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
        }
    }

    mcr.setDiskWriteResult(false, false);
}

总结一下 apply() 的两步操作:

1、把以键值对写入的数据保存到内存(commitToMemory())。

2、把保存到内存的数据加入到一个异步队列中, 等待调度,即异步将数据写入磁盘(enqueueDiskWrite)。

apply() 分析完了,再看看 commit()。

// SharedPreferencesImpl.EditorImpl.java
public boolean commit() {
    long startTime = 0;

    MemoryCommitResult mcr = commitToMemory();

    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
    try {
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    } finally {
        ...
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}

commit() 和 apply() 大体一样,区别如下:

  • apply():同步写入内存,异步写入磁盘,没有返回值。
  • commit():回写逻辑同 apply(),不同的是 commit() 需要等异步回写磁盘完成后才返回,有返回值。
(5)通过 SharedPreferences 对象读取之前保存的值
String name = sharedPreferences.getString("name", "");
String age = sharedPreferences.getInt("rank", "");

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

因为有 synchronized 关键字修饰,所以,getXxx 是线程安全的。

这里也调用了 awaitLoadedLocked() 方法,当第一次创建 SharedPreference 后,马上调用 getXxx,这时很可能文件还没有加载完成,需要等待到加载完成后,才能进行后续操作。

总结一下:

  • 因为内部有大量 synchronized 关键字保障,所以。SharedPreferences 是线程安全的。
  • 因为首次是从磁盘读取,之后都是从内存读取,所以,SharedPreferences 不是进程安全的。

三、如何保证 SharedPreferences 多进程通信的安全?(多进程解决方案)

有四种方法:

  • MODE_MULTI_PROCESS(API level 23 已经被废弃)
  • 继承 ContentProvider 并实现 SharedPreferences 接口
  • 借助 ContentProvider
  • mmkv

1、MODE_MULTI_PROCESS

// ContextImpl.java
public SharedPreferences getSharedPreferences(File file, int mode) {
    ...
    
    if ((mode & Context.MODE_MULTI_PROCESS) != 0 || getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
        sp.startReloadIfChangedUnexpectedly();
    }
    
    return sp;
}

创建 SharedPreferences 对象时,如果 mode 等于 MODE_MULTI_PROCESS,并且 targetVersion 小于 11,会检查磁盘文件上次修改时间和文件大小,一旦有修改则会重新从磁盘加载文件。

但这并不能保证多进程数据的实时同步。

目前该 mode 上有一个注释:

* @deprecated This constant was deprecated in API level 23.
* MODE_MULTI_PROCESS does not work reliably in some versions of Android, and furthermore does not provide any
* mechanism for reconciling concurrent modifications across processes.  Applications should not attempt to use it.  Instead,
* they should use an explicit cross-process data management approach such as {@link android.content.ContentProvider ContentProvider}.

MODE_MULTI_PROCESS 在 API level 23 已经被废弃,推荐使用 ContentProvider,所以这种方式已经不可用。

2、继承 ContentProvider 并实现 SharedPreferences 接口

这种思路有一个现成的开源库:MultiprocessSharedPreferences

MultiprocessSharedPreferences 使用 ContentProvider 实现多进程 SharedPreferences 读写:

1、ContentProvider天生支持多进程访问。

2、使用内部私有 BroadcastReceiver 实现多进程 OnSharedPreferenceChangeListener 监听。

3、借助 ContentProvider

这种方案同样也用到了 ContentProvider,但只是借助了一下 ContentProvider 进行了进程切换。

思路:SharedPreferences 的 get 和 put 放在同一个进程(PROCESS_1)进行操作,使用 ContentProvider 进行进程切换。

// SharedPreferencesManager.java
public void setString(String key, String value) {
    if (PROCESS_1.equals(getProcessName())) {
        // 不管哪个进程调用,最终都会在 PROCESS_1 进程进行保存操作
        SharedPreferences.Editor editor = mSharedPreferences.edit();
        editor.putString(key, value);
        editor.apply();
    } else {
        // 如果非 PROCESS_1 进程调用,会走到这里,这里在 MyContentProvider 中进行进程切换,切换到 PROCESS_1
        MyContentProvider.setStringValue(mContext, key, value);
    }
}

// SharedPreferencesManager.java
public String getString(String key, String defValue) {
    if (PROCESS_1.equals(getProcessName())) {
        // 不管哪个进程调用,最终都会在 PROCESS_1 进程进行读取操作
        return mSharedPreferences.getString(key, defValue);
    } else {
        // 如果非 PROCESS_1 进程调用,会走到这里,这里在 MyContentProvider 中进行进程切换,切换到 PROCESS_1
        return MyContentProvider.getStringValue(mContext, key, defValue);
    }
}

// MyContentProvider.java
public static void setStringValue(Context context, String key, String value) {
    ContentValues contentvalues = new ContentValues();
    contentvalues.put(EXTRA_TYPE, TYPE_STRING);
    contentvalues.put(EXTRA_KEY, key);
    contentvalues.put(EXTRA_VALUE, value);

    try {
        // 通过重写 ContentProvider 的 update() 方法进行保存操作的进程切换
        context.getContentResolver().update(MY_CONTENT_PROVIDER_URI, contentvalues, null, null);
    } catch (Exception e ) {
        e.printStackTrace();
    }
}

// MyContentProvider.java
// 通过重写 ContentProvider 的 update() 方法进行保存操作的进程切换
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
    if (values == null) {
        return 0;
    }

    int type = values.getAsInteger(EXTRA_TYPE);
    if (type == TYPE_STRING) {
        // 这里再调回到 setString() 时,已经被切换到了 PROCESS_1 进程
        SharedPreferencesManager.getInstance(getContext()).setString( values.getAsString(EXTRA_KEY), 
            values.getAsString(EXTRA_VALUE));
    }

    return 1;
}

// MyContentProvider.java
public static String getStringValue(Context context, String key, String defValue){
    ContentValues contentvalues = new ContentValues();
    contentvalues.put(EXTRA_TYPE, TYPE_STRING);
    contentvalues.put(EXTRA_KEY, key);
    contentvalues.put(EXTRA_VALUE, defValue);

    Uri result;

    try {
        // 通过重写 ContentProvider 的 insert() 方法进行读取操作的进程切换
        result = context.getContentResolver().insert(MY_CONTENT_PROVIDER_URI, contentvalues);
    } catch (Exception e) {
        return defValue;
    }

    if (result == null) {
        return defValue;
    }

    return result.toString().substring(LENGTH_CONTENT_URI);
}

// MyContentProvider.java
// 重写 ContentProvider 的 insert() 方法进行保存操作的进程切换
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
    if (values == null) {
        return null;
    }

    String res = "";
    int type = values.getAsInteger(EXTRA_TYPE);
    if (type == TYPE_STRING) {
        // 这里再调回到 getString() 时,已经被切换到了 PROCESS_1 进程
        res += SharedPreferencesManager.getInstance(getContext()).getString(
                values.getAsString(EXTRA_KEY), values.getAsString(EXTRA_VALUE));
    }

    return Uri.parse(MY_CONTENT_PROVIDER_URI.toString() + "/" + res);
}

总结一下这种方案:

1、选一个进程作为 get 和 put 操作的进程(这里选的 PROCESS_1),每次 get 和 put 之前都判断进程,只有选定的进程才能操作 SharedPreferences,其他进程通过 ContentProvider 进行进程切换。

2、重写 ContentProvider 的 insert() 和 update() 方法进行进程切换。

写了个 Demo 放在了 Github 上:MyMultiProcessSharedpreferences

以上两种方案都是使用 ContentProvider 实现,优点是数据同步不易出错,简单好用易上手,缺点是慢,启动慢,访问也慢,所以就有了第四种方案,往下看。

4、mmkv

MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。从 2015 年中至今在微信上使用,其性能和稳定性经过了时间的验证。也已移植到 Android / macOS / Windows 平台,一并开源。

Github 链接:MMKV

参考:

彻底搞懂 SharedPreferences

SharedPreferences 多进程解决方案

Android:这是一份全面 & 详细的SharePreferences学习指南

我的博客即将同步至 OSCHINA 社区,这是我的 OSCHINA ID:osc_17623454,邀请大家一同入驻:https://www.oschina.net/sharing-plan/apply

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,284评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,115评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,614评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,671评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,699评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,562评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,309评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,223评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,668评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,859评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,981评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,705评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,310评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,904评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,023评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,146评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,933评论 2 355

推荐阅读更多精彩内容