阅读 119

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学习指南

关注下面的标签,发现更多相似文章
评论