SharedPreferences 源碼解析及應(yīng)用(多進(jìn)程解決方案)

偶然看到一個(gè)問題畜埋,SharedPreferences 是線程安全的嗎翻伺?SharedPreferences 是進(jìn)程安全的嗎?如果不是虹统,那如何保證線程安全弓坞、進(jìn)程安全呢?

SharedPreferences 經(jīng)常用车荔,也知道不是進(jìn)程安全的渡冻,但這都是從書里看到的,并沒有深入去研究過忧便,這次從源碼角度來分析一下以上這幾個(gè)問題族吻。

一、SharedPreferences 是線程安全的嗎珠增?SharedPreferences 是進(jìn)程安全的嗎超歌?

  • SharedPreferences 是線程安全的,因?yàn)閮?nèi)部有大量 synchronized 關(guān)鍵字保障蒂教。
  • SharedPreferences 不是進(jìn)程安全的巍举,因?yàn)槭状问菑拇疟P讀取,之后都是從內(nèi)存讀取凝垛。

二懊悯、SharedPreferences 源碼解析

1、SharedPreferences 使用

SharedPreferences 的使用分為保存數(shù)據(jù)和讀取數(shù)據(jù)梦皮。

每個(gè) SharedPreferences 都對應(yīng)了當(dāng)前 package 的 data/data/package_name/share_prefs/ 目錄下的一個(gè) xml 文件炭分。保存數(shù)據(jù)和讀取數(shù)據(jù)其實(shí)就是寫入和讀取 xml 文件。

保存數(shù)據(jù)步驟:

  • 獲取 SharedPreferences 對象
  • 通過 Editor 獲取編輯器對象
  • 以鍵值對的形式寫入數(shù)據(jù)
  • 提交修改
// 1剑肯、獲取 SharedPreferences 對象捧毛,有兩種方式
// 方式一
// 參數(shù)1:指定該文件的名稱,參數(shù)2:指定文件的操作模式退子,共有 4 種操作模式岖妄,分別是:
// Context.MODE_PRIVATE = 0:為默認(rèn)操作模式,代表該文件是私有數(shù)據(jù)寂祥,只能被應(yīng)用本身訪問荐虐,在該模式下,寫入的內(nèi)容會(huì)覆蓋原文件的內(nèi)容
// Context.MODE_APPEND = 32768:該模式會(huì)檢查文件是否存在丸凭,存在就往文件追加內(nèi)容福扬,否則就創(chuàng)建新文件腕铸。
// Context.MODE_WORLD_READABLE = 1:表示當(dāng)前文件可以被其他應(yīng)用讀取
// Context.MODE_WORLD_WRITEABLE = 2:表示當(dāng)前文件可以被其他應(yīng)用寫入
SharedPreferences sharedPreferences = context.getSharedPreferences("trampcr_sp", Context.MODE_PRIVATE);
// 方式二
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);

// 2、獲取編輯器對象
Editor editor = sharedPreferences.edit();

// 3铛碑、以鍵值對的方式寫入數(shù)據(jù)
editor.putString("name", "trampcr");
editor.putString("rank", "T6");

// 4狠裹、提交修改,有兩種方式
// 方式一
editor.apply();
// 方式二
editor.commit();

讀取數(shù)據(jù)步驟:

  • 獲取 SharedPreferences 對象
  • 通過 SharedPreferences 對象讀取之前保存的值
// 1汽烦、獲取 SharedPreferences 對象涛菠,有兩種形式,上邊已經(jīng)寫過了撇吞,這里只寫一種形式
SharedPreferences sharedPreferences = getSharedPreferences("ljq", Context.MODE_PRIVATE);

// 2俗冻、通過 SharedPreferences 對象 的 getXxx() 方法讀取之前保存的值(Xxx 為數(shù)據(jù)類型)
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);
}

跟到這里發(fā)現(xiàn)調(diào)用了 mBase.getSharedPreferences(name, mode) 方法迄薄,那這個(gè) mBase 究竟是什么東西呢?

搜了一下 mBase 定義和賦值:

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

從以上可以看出煮岁,mBase 是一個(gè) Context 對象讥蔽,在 attachBaseContext() 方法中進(jìn)行賦值,這個(gè)方法有點(diǎn)面熟画机,好像在 ActivityThread 創(chuàng)建 Activity 那里見過類似的創(chuàng)建上下文的代碼冶伞,去 ActivityThread.handleLaunchActivity() 去看看。

// ActivityThread.java 無關(guān)代碼都先刪了
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;
}

這里先簡單總結(jié)一下上邊的內(nèi)容步氏,再往后分析碰缔。

ActivityThread.handleLaunchActivity() 
-> performLaunchActivity() 
-> createBaseContextForActivity() 
-> createActivityContext()
-> 回到 performLaunchActivity() 調(diào)用 activity.attach(appContext,...)

以上過程可以簡單的理解為創(chuàng)建一個(gè) 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) {
    // 果然調(diào)用了 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 中創(chuàng)建了 ContextImpl 對象瀑焦,并賦值給 mBase腌且,所以 mBase.getSharedPreferences() 就是 ContextImpl.getSharedPreferences()。

這正是獲取 SharedPreferences 的第二種方式榛瓮,所以铺董,第一種方式 PreferenceManager.getDefaultSharedPreferences() 其實(shí)就是對第二種方式 ContextImpl.getSharedPreferences() 的封裝,最終實(shí)現(xiàn)都在 ContextImpl.getSharedPreferences() 中禀晓。

// ContextImpl.java
public SharedPreferences getSharedPreferences(File file, int mode) {
    ...
    
    SharedPreferencesImpl sp;
    
    // 創(chuàng)建 SharedPreferences 對象使用 synchronized 關(guān)鍵字修飾精续,所以創(chuàng)建實(shí)例過程是線程安全的
    synchronized (ContextImpl.class) {
        // 先從緩存中獲取 SharedPreferences 對象
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        sp = cache.get(file);
        if (sp == null) {
            // 如果沒有緩存,則創(chuàng)建
            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 對象先從緩存中獲取粹懒,如果緩存中沒有重付,則創(chuàng)建;同時(shí)凫乖,實(shí)例的創(chuàng)建是被 synchronized 修飾的确垫,所以創(chuàng)建 SharedPreferences 對象的過程是線程安全的弓颈。

接下來看下 SharedPreferences 對象的創(chuàng)建:

// SharedPreferencesImpl.java
SharedPreferencesImpl(File file, int mode) {
    mFile = file; 
    mBackupFile = makeBackupFile(file); // 備份文件,用于寫入失敗時(shí)進(jìn)行恢復(fù)
    mMode = mode;
    mLoaded = false;
    mMap = null; // 在內(nèi)存中緩存的數(shù)據(jù)集合, 也就是 getXxx() 數(shù)據(jù)的來源
    startLoadFromDisk();
}

// SharedPreferencesImpl.java
private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    
    // 開啟一個(gè)線程從磁盤文件讀取數(shù)據(jù)
    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、從磁盤讀取文件到內(nèi)存
                str = new BufferedInputStream(new FileInputStream(mFile), 16*1024);
                map = XmlUtils.readMapXml(str);
            } 
            
            ...
        }
    } catch (ErrnoException e) {
    }

    synchronized (mLock) {
        // 3披泪、標(biāo)記讀取完成纤子,這個(gè)字段后面 awaitLoadedLocked() 方法會(huì)用到
        mLoaded = true;
        if (map != null) {
            // 4、將從磁盤讀取到的文件內(nèi)容保存在 mMap 字段中
            mMap = map;
            
            // 5款票、記錄讀取文件時(shí)間控硼,后面 MODE_MULTI_PROCESS 中會(huì)用到
            mStatTimestamp = stat.st_mtime;
            mStatSize = stat.st_size;
        } else {
            mMap = new HashMap<>();
        }
        
        // 6、發(fā)一個(gè) notifyAll() 通知已經(jīng)讀取完畢徽职,喚醒所有等待加載的其他線程
        mLock.notifyAll();
    }
}

SharedPreferencesImpl 構(gòu)造方法中調(diào)用 startLoadFromDisk()象颖,在 startLoadFromDisk() 中開啟了一個(gè)線程 調(diào)用 loadFromDisk() 從磁盤文件讀取數(shù)據(jù),它做了如下幾件事:

  1. 如果有備份文件姆钉,則直接使用備份文件(重命名)
  2. 從磁盤讀取文件到內(nèi)存
  3. 標(biāo)記讀取完成(mLoaded = true;)说订,這個(gè)字段后面 awaitLoadedLocked() 方法中會(huì)用到
  4. 將從磁盤讀取到的文件內(nèi)容保存在 mMap 字段中
  5. 記錄讀取文件的時(shí)間(mStatTimestamp = stat.st_mtime;),后面 MODE_MULTI_PROCESS 中會(huì)用到
  6. 發(fā)一個(gè) notifyAll() 通知已經(jīng)讀取完畢潮瓶,喚醒所有等待加載的其他線程
(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 {
            // 當(dāng)沒有讀取完配置文件陶冷,先等待,此時(shí)不能返回 Editor毯辅,也不能保存數(shù)據(jù)
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
}

在 new EditorImpl() 之前埂伦,先調(diào)用 awaitLoadedLocked(),如果 mLoaded = false思恐,即沒有讀取完配置文件時(shí)沾谜,會(huì)卡在這里,直到 SharedPreferencesImpl.loadFromDisk() 讀取完畢后調(diào)用 notifyAll() 通知所有等待的線程才會(huì)返回 EditorImpl 對象胀莹。

EditorImpl 是 SharedPreferencesImpl 的一個(gè)內(nèi)部類基跑,沒有構(gòu)造方法,只有兩個(gè)屬性被初始化描焰。

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

    @GuardedBy("mLock")
    // 保存 putXxx 傳入數(shù)據(jù)的集合
    private final Map<String, Object> mModified = Maps.newHashMap();

    @GuardedBy("mLock")
    private boolean mClear = false;
    
    ...
}
(3)以鍵值對的形式寫入數(shù)據(jù)
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媳否、把以鍵值對寫入的數(shù)據(jù)保存到內(nèi)存
    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、把保存到內(nèi)存的數(shù)據(jù)加入到一個(gè)異步隊(duì)列中, 等待調(diào)度
    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 稍后會(huì)被寫到磁盤
        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 中的數(shù)據(jù)寫到 SharedPreferences.mMap 中, 這一步完成了內(nèi)存的同步
                    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;

        // 只有當(dāng)磁盤的狀態(tài)比目前的提交狀態(tài)老的時(shí)候才寫磁盤
        if (mDiskStateGeneration < mcr.memoryStateGeneration) {
            if (isFromSyncCommit) {
                needsWrite = true;
            } else {
                synchronized (mLock) {
                    if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                        needsWrite = true;
                    }
                }
            }
        }
        
        ...

        boolean backupFileExists = mBackupFile.exists();

        if (!backupFileExists) {
            // 把已經(jīng)存在的老文件重命名(加 .bak 后綴)為備份文件
            if (!mFile.renameTo(mBackupFile)) {
                mcr.setDiskWriteResult(false, false);
                return;
            }
        } else {
            // 如果已經(jīng)有了備份文件,則刪除老的配置文件
            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) {
                // 記錄寫入到磁盤的時(shí)間
                mStatTimestamp = stat.st_mtime;
                mStatSize = stat.st_size;
            }
        } catch (ErrnoException e) {
            // Do nothing
        }

        // 如果寫入磁盤成功則刪除備份文件
        mBackupFile.delete();

        ...

        return;
    } 
    
    ...
    
    if (mFile.exists()) {
        // 如果寫入磁盤失敗, 則刪除這個(gè)文件
        if (!mFile.delete()) {
            Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
        }
    }

    mcr.setDiskWriteResult(false, false);
}

總結(jié)一下 apply() 的兩步操作:

1步绸、把以鍵值對寫入的數(shù)據(jù)保存到內(nèi)存(commitToMemory())掺逼。

2、把保存到內(nèi)存的數(shù)據(jù)加入到一個(gè)異步隊(duì)列中, 等待調(diào)度瓤介,即異步將數(shù)據(jù)寫入磁盤(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() 大體一樣兽泄,區(qū)別如下:

  • apply():同步寫入內(nèi)存漓概,異步寫入磁盤,沒有返回值病梢。
  • 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;
    }
}

因?yàn)橛?synchronized 關(guān)鍵字修飾觅彰,所以,getXxx 是線程安全的钮热。

這里也調(diào)用了 awaitLoadedLocked() 方法填抬,當(dāng)?shù)谝淮蝿?chuàng)建 SharedPreference 后晚顷,馬上調(diào)用 getXxx丁频,這時(shí)很可能文件還沒有加載完成,需要等待到加載完成后把鉴,才能進(jìn)行后續(xù)操作仆潮。

總結(jié)一下:

  • 因?yàn)閮?nèi)部有大量 synchronized 關(guān)鍵字保障宏蛉,所以。SharedPreferences 是線程安全的性置。
  • 因?yàn)槭状问菑拇疟P讀取拾并,之后都是從內(nèi)存讀取,所以鹏浅,SharedPreferences 不是進(jìn)程安全的嗅义。

三、如何保證 SharedPreferences 多進(jìn)程通信的安全隐砸?(多進(jìn)程解決方案)

有四種方法:

  • MODE_MULTI_PROCESS(API level 23 已經(jīng)被廢棄)
  • 繼承 ContentProvider 并實(shí)現(xiàn) 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;
}

創(chuàng)建 SharedPreferences 對象時(shí),如果 mode 等于 MODE_MULTI_PROCESS凰萨,并且 targetVersion 小于 11,會(huì)檢查磁盤文件上次修改時(shí)間和文件大小械馆,一旦有修改則會(huì)重新從磁盤加載文件胖眷。

但這并不能保證多進(jìn)程數(shù)據(jù)的實(shí)時(shí)同步。

目前該 mode 上有一個(gè)注釋:

* @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 已經(jīng)被廢棄霹崎,推薦使用 ContentProvider珊搀,所以這種方式已經(jīng)不可用。

2尾菇、繼承 ContentProvider 并實(shí)現(xiàn) SharedPreferences 接口

這種思路有一個(gè)現(xiàn)成的開源庫:MultiprocessSharedPreferences

MultiprocessSharedPreferences 使用 ContentProvider 實(shí)現(xiàn)多進(jìn)程 SharedPreferences 讀寫:

1境析、ContentProvider天生支持多進(jìn)程訪問囚枪。

2、使用內(nèi)部私有 BroadcastReceiver 實(shí)現(xiàn)多進(jìn)程 OnSharedPreferenceChangeListener 監(jiān)聽劳淆。

3链沼、借助 ContentProvider

這種方案同樣也用到了 ContentProvider,但只是借助了一下 ContentProvider 進(jìn)行了進(jìn)程切換沛鸵。

思路:SharedPreferences 的 get 和 put 放在同一個(gè)進(jìn)程(PROCESS_1)進(jìn)行操作括勺,使用 ContentProvider 進(jìn)行進(jìn)程切換。

// SharedPreferencesManager.java
public void setString(String key, String value) {
    if (PROCESS_1.equals(getProcessName())) {
        // 不管哪個(gè)進(jìn)程調(diào)用曲掰,最終都會(huì)在 PROCESS_1 進(jìn)程進(jìn)行保存操作
        SharedPreferences.Editor editor = mSharedPreferences.edit();
        editor.putString(key, value);
        editor.apply();
    } else {
        // 如果非 PROCESS_1 進(jìn)程調(diào)用疾捍,會(huì)走到這里,這里在 MyContentProvider 中進(jìn)行進(jìn)程切換栏妖,切換到 PROCESS_1
        MyContentProvider.setStringValue(mContext, key, value);
    }
}

// SharedPreferencesManager.java
public String getString(String key, String defValue) {
    if (PROCESS_1.equals(getProcessName())) {
        // 不管哪個(gè)進(jìn)程調(diào)用乱豆,最終都會(huì)在 PROCESS_1 進(jìn)程進(jìn)行讀取操作
        return mSharedPreferences.getString(key, defValue);
    } else {
        // 如果非 PROCESS_1 進(jìn)程調(diào)用,會(huì)走到這里吊趾,這里在 MyContentProvider 中進(jìn)行進(jìn)程切換宛裕,切換到 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() 方法進(jìn)行保存操作的進(jìn)程切換
        context.getContentResolver().update(MY_CONTENT_PROVIDER_URI, contentvalues, null, null);
    } catch (Exception e ) {
        e.printStackTrace();
    }
}

// MyContentProvider.java
// 通過重寫 ContentProvider 的 update() 方法進(jìn)行保存操作的進(jìn)程切換
@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) {
        // 這里再調(diào)回到 setString() 時(shí),已經(jīng)被切換到了 PROCESS_1 進(jìn)程
        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() 方法進(jìn)行讀取操作的進(jìn)程切換
        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() 方法進(jìn)行保存操作的進(jìn)程切換
@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) {
        // 這里再調(diào)回到 getString() 時(shí)趾徽,已經(jīng)被切換到了 PROCESS_1 進(jìn)程
        res += SharedPreferencesManager.getInstance(getContext()).getString(
                values.getAsString(EXTRA_KEY), values.getAsString(EXTRA_VALUE));
    }

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

總結(jié)一下這種方案:

1续滋、選一個(gè)進(jìn)程作為 get 和 put 操作的進(jìn)程(這里選的 PROCESS_1),每次 get 和 put 之前都判斷進(jìn)程孵奶,只有選定的進(jìn)程才能操作 SharedPreferences疲酌,其他進(jìn)程通過 ContentProvider 進(jìn)行進(jìn)程切換。

2了袁、重寫 ContentProvider 的 insert() 和 update() 方法進(jìn)行進(jìn)程切換朗恳。

寫了個(gè) Demo 放在了 Github 上:MyMultiProcessSharedpreferences

以上兩種方案都是使用 ContentProvider 實(shí)現(xiàn),優(yōu)點(diǎn)是數(shù)據(jù)同步不易出錯(cuò)载绿,簡單好用易上手粥诫,缺點(diǎn)是慢,啟動(dòng)慢崭庸,訪問也慢怀浆,所以就有了第四種方案,往下看怕享。

4执赡、mmkv

MMKV 是基于 mmap 內(nèi)存映射的 key-value 組件,底層序列化/反序列化使用 protobuf 實(shí)現(xiàn)函筋,性能高沙合,穩(wěn)定性強(qiáng)。從 2015 年中至今在微信上使用跌帐,其性能和穩(wěn)定性經(jīng)過了時(shí)間的驗(yàn)證首懈。也已移植到 Android / macOS / Windows 平臺(tái)绊率,一并開源。

Github 鏈接:MMKV

參考:

徹底搞懂 SharedPreferences

SharedPreferences 多進(jìn)程解決方案

Android:這是一份全面 & 詳細(xì)的SharePreferences學(xué)習(xí)指南

我的博客即將同步至 OSCHINA 社區(qū)究履,這是我的 OSCHINA ID:osc_17623454滤否,邀請大家一同入駐:https://www.oschina.net/sharing-plan/apply

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市挎袜,隨后出現(xiàn)的幾起案子顽聂,更是在濱河造成了極大的恐慌,老刑警劉巖盯仪,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件紊搪,死亡現(xiàn)場離奇詭異,居然都是意外死亡全景,警方通過查閱死者的電腦和手機(jī)耀石,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來爸黄,“玉大人滞伟,你說我怎么就攤上這事】还螅” “怎么了梆奈?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長称开。 經(jīng)常有香客問我亩钟,道長,這世上最難降的妖魔是什么鳖轰? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任清酥,我火速辦了婚禮,結(jié)果婚禮上蕴侣,老公的妹妹穿的比我還像新娘焰轻。我一直安慰自己,他們只是感情好昆雀,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布辱志。 她就那樣靜靜地躺著,像睡著了一般狞膘。 火紅的嫁衣襯著肌膚如雪揩懒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天客冈,我揣著相機(jī)與錄音,去河邊找鬼稳强。 笑死场仲,一個(gè)胖子當(dāng)著我的面吹牛和悦,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播渠缕,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼鸽素,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了亦鳞?” 一聲冷哼從身側(cè)響起馍忽,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎燕差,沒想到半個(gè)月后遭笋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡徒探,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年瓦呼,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片测暗。...
    茶點(diǎn)故事閱讀 38,646評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡央串,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出碗啄,到底是詐尸還是另有隱情质和,我是刑警寧澤,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布稚字,位于F島的核電站饲宿,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏尉共。R本人自食惡果不足惜褒傅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望袄友。 院中可真熱鬧殿托,春花似錦、人聲如沸剧蚣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鸠按。三九已至礼搁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間目尖,已是汗流浹背馒吴。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人饮戳。 一個(gè)月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓豪治,卻偏偏與公主長得像,于是被迫代替她去往敵國和親扯罐。 傳聞我的和親對象是個(gè)殘疾皇子负拟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,514評論 2 348

推薦閱讀更多精彩內(nèi)容