Android源碼之SharedPreferences

0. 前言

SharedPreferences可以說是Android中最常用的一種存數據到文件的方式。他的數據是以鍵值對的方式存儲在 ~/data/data/包名/shared_prefs 這個文件夾中的。

這個存儲框架是非常輕量級的,如果我們需要存一些小數據或者是一個小型的可序列化的Bean實體類的,使用SharedPreferences是最明智的選擇。

1. 使用方法

1.1 獲取SharedPreferences

在使用SharedPreferences前,我們得先獲取到它。

由於SharedPreferences是Android內置的一個框架,所以我們想要獲取到它非常的簡單,不需要導入任何依賴,直接寫代碼就行。下面我們就來介紹下獲取對象的三個方式:

1.1.1 Context # getSharedPreferences()

首先就是可以說是最常用的方法,通過Context的getSharedPreferences() 方法去獲取到SharedPreferences對象。由於是通過Context獲取的,所以基本上Android的所有場景我們都可以通過這個方法獲取到。

public abstract SharedPreferences getSharedPreferences (String name, 
                int mode)

這個方法接收兩個參數,分別是namemode

  • name:name就是我們要存儲的SharedPreferences本地文件的名字,這個可以自定義。但是如果使用同樣的name的話,永遠只能獲取到同一個SharedPreferences的對象。
  • mode:mode就是我們要獲取的這個SharedPreferences的訪問模式,Android給我們提供了挺多的模式的,但是由於其餘的模式或多或少存在着安全隱患(因爲其他應用也可以直接獲取到),所以就全部都棄用了,現在就只有一個MODE_PRIVATE模式。

此外,這個方法是線程安全的。

Mode的可選參數:

  • MODE_PRIVATE:私有模式,該SharedPreferences只會被調用他的APP去使用,其他的APP無法獲取到這個SharedPreferences。
  • MODE_WORLD_READABLE:API17被棄用。使用這個模式,所有的APP都可以對這個SharedPreferences進行讀操作。所以這個模式被Android官方嚴厲警告禁止使用(It is strongly discouraged),並推薦使用ContentProviderBroadcastReceiverService
  • MODE_WORLD_WRITEABLE:API17被棄用。和上面類似,這個是可以被所有APP進行寫操作。同樣也是被嚴厲警告禁止使用。
  • MODE_MULTI_PROCESS:API23被棄用。使用了這個模式,允許多個進程對同一個SharedPreferences進行操作,但是後來也被啓用了,原因是因爲在某些Android版本下,這個模式不能可靠的運行,官方建議如果多進程建議使用ContentProvider去操作。在後面我們會說爲啥多進程下不可靠。

1.1.2 Activity # getPreferences()

這個方法只能在Activity中或者通過Activity對象去使用。

public SharedPreferences getPreferences (int mode)

這個方法需要傳入一個mode參數,這個參數和上面的context#getSharedPreferences()mode參數是一樣的。其實這個方法和上面Context的那個方法是一樣的,他兩都是調用的SharedPreferences getSharedPreferences(String name, int mode)。只不過Context的需要你去指定文件名,而這個方法你不需要手動去指定,而是會自動將當前Activity的類名作爲了文件名。

1.1.3 PreferencesManager # getDefaultSharedPreferences()

這個一般用在Android的設置頁面上,或者說,我們也只有在構建設置頁面的時候纔會去使用這個。

public static SharedPreferences getDefaultSharedPreferences (Context context)

他承接一個context參數,並自動將當前應用的報名作爲前綴來命名文件。

1.2 存數據

如果需要往SharedPreferences中存儲數據的話,我們並不能直接對SharedPreferences對象進行操作,因爲SharedPreferences沒有提供存儲或者修改數據的接口。

如果想要對SharedPreferences存儲的數據進行修改,需要通過SharedPreferences.edit()方法去獲取到SharedPreferences.Editor對象來進行操作。

獲取到Editor對象後,我們就可以調用他的putXXX()方法進行存儲了,存儲之後一定記得通過apply()commit()方法去將數據提交。

至於commitapply的區別我們後面會說。

 //步驟1:創建一個SharedPreferences對象
 SharedPreferences sharedPreferences= getSharedPreferences("data",Context.MODE_PRIVATE);
 //步驟2: 實例化SharedPreferences.Editor對象
 SharedPreferences.Editor editor = sharedPreferences.edit();
 //步驟3:將獲取過來的值放入文件
 editor.putString("name", “Tom”);
 editor.putInt("age", 28);
 editor.putBoolean("marrid",false);
 //步驟4:提交               
 editor.commit();
 
// 刪除指定數據
 editor.remove("name");
 editor.commit();
 
// 清空數據
 editor.clear();
 editor.commit();

1.3 取數據

取值就很簡單了,構建出SharedPreferences的對象後,就直接調用SharedPreferences的getXXX()方法就行。

SharedPreferences sharedPreferences = getSharedPreferences("data", Context .MODE_PRIVATE);
String userId = sharedPreferences.getString("name", "");

2. 源碼分析

2.1 獲取SharedPreferences實例

我們上面說到,獲取SharedPreferences實例最常用的方法就是Context#getSharedPreferences()。那我們就從這個方法入手,看到底是怎麼獲取到SharedPreferences實例的。

我們先看下這個方法的實現:

public class ContextWrapper extends Context {
    @UnsupportedAppUsage
    Context mBase;
    
    // ...
    @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        return mBase.getSharedPreferences(name, mode);
    }
}

可以看到他又調用了Context的getSharedPreferences()方法:

public abstract SharedPreferences getSharedPreferences(String name, @PreferencesMode int mode);

然後我們就會驚喜的發現,這是一個抽象方法。我開始還想去找一個ContextWrapper的構造的地方,看看mBase傳入的是啥,後來找了一圈沒找到,直接上網搜索,立馬得到答案:ContextImpl,這個可以說是Context在Android中的唯一實現類,所有的操作又得經過這個類。那麼我們就來看下這個類中的getSharedPreferences()方法的實現:

@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
    // At least one application in the world actually passes in a null
    // name.  This happened to work because when we generated the file name
    // we would stringify it to "null.xml".  Nice. 
            // ps:這個nice很精髓😂
    if (mPackageInfo.getApplicationInfo().targetSdkVersion <
            Build.VERSION_CODES.KITKAT) {
        if (name == null) {
            name = "null";
        }
    }

    File file;
    // 加了一個類鎖,保證同步
    synchronized (ContextImpl.class) {
        // mSharedPrefsPaths是一個保存了name和file對應關係的ArrayMap
        if (mSharedPrefsPaths == null) {
            mSharedPrefsPaths = new ArrayMap<>();
        }
        // 根據name從裏面找有沒有緩存的file
        file = mSharedPrefsPaths.get(name);
        // 如果沒有,那就調用getSharedPreferencesPath去找
        if (file == null) {
            // ->>> 重點1. getSharedPreferencesPath(name)
            file = getSharedPreferencesPath(name);
            // 並保存到mSharedPrefsPaths
            mSharedPrefsPaths.put(name, file);
        }
    }
    // 獲取到file後,再調用getSharedPreferences
    return getSharedPreferences(file, mode);
}

/**
 * 重點1. ContextImpl # getSharedPreferencesPath(String name)
 *   根據PreferencesDir和name.xml去創建了這個文件
 */
@Override
public File getSharedPreferencesPath(String name) {
    return makeFilename(getPreferencesDir(), name + ".xml");
}

那我們在看下getSharedPreferences(File file, int mode)的實現:

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    // SharedPreferences唯一實現類SharedPreferencesImpl的實例
    SharedPreferencesImpl sp;
    // 同樣的加類鎖
    synchronized (ContextImpl.class) {
        // 構造了一個File-SharedPreferencesImpl對應關係的ArrayMap
        // 調用getSharedPreferencesCacheLocked方法區獲取cahce
        // ->>> 重點1
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        // 從file-SharedPreferencesImpl鍵值對中根據當前file去過去SharedPreferencesImpl實例
        sp = cache.get(file);
        // 如果沒有,那就需要新建一個
        if (sp == null) {
            // 檢查mode,如果是MODE_WORLD_WRITEABLE或者MODE_MULTI_PROCESS則直接拋異常
            checkMode(mode);
            if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
                if (isCredentialProtectedStorage()
                        && !getSystemService(UserManager.class)
                                .isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
                    throw new IllegalStateException("SharedPreferences in credential encrypted "
                            + "storage are not available until after user is unlocked");
                }
            }
            // 調用構造方法去構造SharedPreferencesImpl對象
            sp = new SharedPreferencesImpl(file, mode);
            // 將對象和file的鍵值對存入cache中
            cache.put(file, sp);
            return sp;
        }
    }
    if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
        getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
        // If somebody else (some other process) changed the prefs
        // file behind our back, we reload it.  This has been the
        // historical (if undocumented) behavior.
        sp.startReloadIfChangedUnexpectedly();
    }
    return sp;
}

/**
 * 重點1. ContextImap # getSharedPreferencesCacheLocked()
 *   根據當前的包名,去獲取到由此應用創建的File-SharedPreferencesImpl的Map對象,
 *       而這個對象裏面就存放了這個應用創建的所有的SharedPreferencesImpl和File的對應關係
 */
@GuardedBy("ContextImpl.class")
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
    // 如果sSharedPrefsCache爲空就構造一個ArrayMap
    // sSharedPrefsCache就是一個存放String-String, ArrayMap<File, SharedPreferencesImpl>的Map
    // 換句話說,也就是存放包名-packagePrefs對應關係的Map
    if (sSharedPrefsCache == null) {
        sSharedPrefsCache = new ArrayMap<>();
    }

    // 獲取包名
    final String packageName = getPackageName();
    // 到sSharedPrefsCache中找
    ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
    // 如果找不到,就構建一個然後存進去
    if (packagePrefs == null) {
        packagePrefs = new ArrayMap<>();
        sSharedPrefsCache.put(packageName, packagePrefs);
    }

    // 找得到就返回
    return packagePrefs;
}

2.2 構建SharedPreferencesImpl

2.2.1 SharedPreferencesImpl構造方法

我們先來看下這個類的構造方法:

@UnsupportedAppUsage
SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    // file的備份文件
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    // 從磁盤加載的標誌,當需要從磁盤加載時將其設爲true,這樣如果有其他線程也調用了SharedPreferences的加載方法時,就會因爲其爲true而直接返回也就不執行加載方法
    // 保證了全局只有一個線程在加載
    mLoaded = false;
    // SharedPreferences中的數據
    mMap = null;
    // 保存的錯誤信息
    mThrowable = null;
    startLoadFromDisk();
}

初始化參數後立馬調用了startLoadFromDisk()方法:

2.2.2 startLoadFromDisk()

@UnsupportedAppUsage
private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    // 開啓一個新線程來加載數據
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}

2.2.3 loadFromDIsk()

loadFromDisk():

private void loadFromDisk() {
    synchronized (mLock) {
        // 如果已經家在過了,就直接退出
        if (mLoaded) {
            return;
        }
        // 如果備份文件已經存在,那就刪除源文件,並將備份文件替換爲源文件
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }

    // Debugging
    if (mFile.exists() && !mFile.canRead()) {
        Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
    }

    // 存儲聚的map
    Map<String, Object> map = null;
    // 文件信息,對應的是C語言stat.h中的struct stat
    StructStat stat = null;
    Throwable thrown = null;
    try {
        // 通過文件路徑去構建StructStat對象
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            // 從XML中把數據讀出來,並把數據轉化成Map類型
            BufferedInputStream str = null;
            try {
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16 * 1024);
                map = (Map<String, Object>) XmlUtils.readMapXml(str);
            } catch (Exception e) {
                Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
            } finally {
                IoUtils.closeQuietly(str);
            }
        }
    } catch (ErrnoException e) {
        // An errno exception means the stat failed. Treat as empty/non-existing by
        // ignoring.
    } catch (Throwable t) {
        thrown = t;
    }

    synchronized (mLock) {
        mLoaded = true;
        mThrowable = thrown;

        // It's important that we always signal waiters, even if we'll make
        // them fail with an exception. The try-finally is pretty wide, but
        // better safe than sorry.
        try {
            if (thrown == null) {
                // 文件裏拿到的數據爲空就重建,存在就賦值
                if (map != null) {
                    // 將數據存儲放置到具體類的一個全局變量中
                    // 稍微記一下這個關鍵點
                    mMap = map;
                    mStatTimestamp = stat.st_mtim;
                    mStatSize = stat.st_size;
                } else {
                    mMap = new HashMap<>();
                }
            }
            // In case of a thrown exception, we retain the old map. That allows
            // any open editors to commit and store updates.
        } catch (Throwable t) {
            mThrowable = t;
        } finally {
            mLock.notifyAll();
        }
    }
}

到目前來說,就完成的SharedPreferencesImpl的構建過程。

2.3 讀數據 SharedPreferences # getXXX()

相對來說,讀數據涉及到的方法比寫數據簡單得多,所以我們先來看下讀數據:
我們以getString()爲例

2.3.1 getString

@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
        // 見2.3.2
        awaitLoadedLocked();
        // 從map中獲取數據
        String v = (String)mMap.get(key);
        // 如果獲取到數據,就返回數據,否則返回方法參數中給定的默認值
        return v != null ? v : defValue;
    }
}

2.3.2 awaitLoadedLocked

@GuardedBy("mLock")
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);
    }
}

這個方法簡單點來說就是如果mLoad不爲true也就是沒有加載完成的話,就等待加載完成。

2.4 寫數據

2.4.1 SharedPreferences.Editor

但是光構建了對象還不夠,我們還得能對她進行操作。我們前面說到過,SharedPreferences並不提供修改的功能,如果你想對她進行修改,必須通過SharedPreferences.Editor來實現。

我們來看下SharedPreferences.edit():

@Override
public Editor edit() {
    // TODO: remove the need to call awaitLoadedLocked() when
    // requesting an editor.  will require some work on the
    // Editor, but then we should be able to do:
    //
    //      context.getSharedPreferences(..).edit().putString(..).apply()
    //
    // ... all without blocking.
    synchronized (mLock) {
        // ->>> 重點1
        awaitLoadedLocked();
    }

    // 創建了一個EditorImpl的對象,
    // 但是這塊需要注意下,我們想對SharedPreferences進行修改,就必須調用edit()方法,就會去構建一個新的EditorImpl對象
    // 所以爲了避免不必要的開銷,我們在使用時最好一次性完成對數據的操作
    return new EditorImpl();
}

/**
 * 重點1:SharedPreferencesImpl # awaitLoadedLocked()
 */
@GuardedBy("mLock")
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);
    }
}

2.4.2 EditorImpl

2.4.2.1 putXXX()

那我們再來看下putXXX()方法,我們以putString()來舉例:

public final class EditorImpl implements Editor {
    private final Object mEditorLock = new Object();

    // 存數據的HashMap
    @GuardedBy("mEditorLock")
    private final Map<String, Object> mModified = new HashMap<>();

    @GuardedBy("mEditorLock")
    private boolean mClear = false;

    @Override
    public Editor putString(String key, @Nullable String value) {
        synchronized (mEditorLock) {
            mModified.put(key, value);
            return this;
        }
    }

putString()方法很簡單,直接將數據put到存數據的HashMap中去就行了。或者說,所有的putXXX()都是這麼簡單。

但是,如果我們想將修改提交到SharedPreferences裏面去的話,還需要調用apply()或者commit()方法,那我們現在來看下這兩個方法。

2.4.2.2 apply()

@Override
public void apply() {
    // 獲取當前時間
    final long startTime = System.currentTimeMillis();

    // 見2.2.3.4
    // 構建了一個MemoryCommitResult的對象
    final MemoryCommitResult mcr = commitToMemory();
    // 新建一個線程,因爲數據操作是很耗時的
    final Runnable awaitCommit = new Runnable() {
            @Override
            public void run() {
                try {
                    // 進入等待狀態
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }

                if (DEBUG && mcr.wasWritten) {
                    Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                            + " applied after " + (System.currentTimeMillis() - startTime)
                            + " ms");
                }
            }
        };

    // 將awaitCommit添加到Queue的Word中去
    QueuedWork.addFinisher(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run() {
                // 執行操作,並從QueuedWord中刪除
                awaitCommit.run();
                QueuedWork.removeFinisher(awaitCommit);
            }
        };

    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

    // Okay to notify the listeners before it's hit disk
    // because the listeners should always get the same
    // SharedPreferences instance back, which has the
    // changes reflected in memory.
    notifyListeners(mcr);
}

2.4.2.3 commit()

@Override
public boolean commit() {
    long startTime = 0;

    if (DEBUG) {
        startTime = System.currentTimeMillis();
    }

    // 見2.2.3.4
    // 構建了一個MemoryCommitResult對象
    MemoryCommitResult mcr = commitToMemory();
    // 將內存數據同步到文件
    // 見
    SharedPreferencesImpl.this.enqueueDiskWrite(
        mcr, null /* sync write on this thread okay */);
    try {
        // 進入等待狀態, 直到寫入文件的操作完成
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    } finally {
        if (DEBUG) {
            Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                    + " committed after " + (System.currentTimeMillis() - startTime)
                    + " ms");
        }
    }
    // 通知監聽則, 並在主線程回調onSharedPreferenceChanged()方法
    notifyListeners(mcr);
    // 返回文件操作的結果數據
    return mcr.writeToDiskResult;
}

2.4.2.4 commitToMemory()

// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
    // 當前Memory的狀態,其實也就是當需要提交數據到內存的時候,他的值就加一
    long memoryStateGeneration;
    List<String> keysModified = null;
    Set<OnSharedPreferenceChangeListener> listeners = null;
    // 存數據的Map
    Map<String, Object> mapToWriteToDisk;

    synchronized (SharedPreferencesImpl.this.mLock) {
        // We optimistically don't make a deep copy until
        // a memory commit comes in when we're already
        // writing to disk.
        // 如果有數據待被提交到硬盤
        if (mDiskWritesInFlight > 0) {
            // We can't modify our mMap as a currently
            // in-flight write owns it.  Clone it before
            // modifying it.
            // noinspection unchecked
            mMap = new HashMap<String, Object>(mMap);
        }
        mapToWriteToDisk = mMap;
        // 2.2.3.5的關鍵點
        mDiskWritesInFlight++;

        boolean hasListeners = mListeners.size() > 0;
        if (hasListeners) {
            keysModified = new ArrayList<String>();
            listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
        }

        synchronized (mEditorLock) {
            boolean changesMade = false;
            
            // 如果mClear爲true,就清空mapToWriteToDisk
            if (mClear) {
                if (!mapToWriteToDisk.isEmpty()) {
                    changesMade = true;
                    mapToWriteToDisk.clear();
                }
                mClear = false;
            }

            for (Map.Entry<String, Object> e : mModified.entrySet()) {
                String k = e.getKey();
                Object v = e.getValue();
                // "this" is the magic value for a removal mutation. In addition,
                // setting a value to "null" for a given key is specified to be
                // equivalent to calling remove on that key.
                if (v == this || v == null) {
                    if (!mapToWriteToDisk.containsKey(k)) {
                        continue;
                    }
                    mapToWriteToDisk.remove(k);
                } else {
                    if (mapToWriteToDisk.containsKey(k)) {
                        Object existingValue = mapToWriteToDisk.get(k);
                        if (existingValue != null && existingValue.equals(v)) {
                            continue;
                        }
                    }
                    mapToWriteToDisk.put(k, v);
                }

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

            mModified.clear();

            if (changesMade) {
                mCurrentMemoryStateGeneration++;
            }

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

這段代碼剛開始看的時候有點暈,但是看完後就瞬間懂了,這段代碼主要執行了一下的功能:

  • mMap賦值給mapToWriteToDisk
  • mClear爲true的時候,清空mapToWriteToDisk
  • 遍歷mModifiedmModified也就是我們上面說到的保存本次edit的數據的HashMap
    • 噹噹前的value爲null或者this的時候,移除對應的k
  • 構建了一個MemoryCommitResult對象

2.4.2.5 SharedPreferencesImpl # 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) {
                    // 這個方法這塊就不講了,太長了,大家感興趣可以看下
                    // 主要功能就是
                    //  1. 當沒有key沒有改變,則直接返回了;否則執行下一步
                    //  2. 將mMap全部信息寫入文件,如果寫入成功則刪除備份文件,如果寫入失敗則刪除mFile
                    writeToFile(mcr, isFromSyncCommit);
                }
                synchronized (mLock) {
                    // 當寫入成功後,將標誌位減1
                    mDiskWritesInFlight--;
                }
                // 此時postWriteRunnable爲null不執行該方法
                if (postWriteRunnable != null) {
                    postWriteRunnable.run();
                }
            }
        };

    // Typical #commit() path with fewer allocations, doing a write on
    // the current thread.
    // 如果是commit則進入
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            // 由於commitToMemory會讓mDiskWritesInFlight+1,則wasEmpty爲true
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            // 在執行一遍上面的操作,保證將commit的內容也保存
            writeToDiskRunnable.run();
            return;
        }
    }
    // 如果是apply()方法,則會將任務放入單線程的線程池中去執行
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

所以從這個方法我們可以看到:

  • commit()是直接同步執行的,有數據就存入磁盤
  • apply()是先將awaitCommit放入QueuedWork,然後在單線程的線程池中去執行,執行完畢後再將awaitCommitQeueudWork中移除。

3. 知識點

3.1 apply和commit的區別

  • apply沒有返回值, commit有返回值能知道修改是否提交成功
  • apply是將修改提交到內存,再異步提交到磁盤文件; commit是同步的提交到磁盤文件;
  • 多併發的提交commit時,需等待正在處理的commit數據更新到磁盤文件後纔會繼續往下執行,從而降低效率; 而apply只是原子更新到內存,後調用apply函數會直接覆蓋前面內存數據,從一定程度上提高很多效率。

3.2 多進程的問題

我們前面說到了,SP提供了多進程訪問,雖說沒有像World模式那樣會直接拋異常,但是官方不建議多進程下使用SP。

那麼我們不禁會好奇,多進程下訪問SP會有什麼問題呢?

探究這個問題,我們得先回到ContextImpl#getSharedPreferences(File file, int mode)方法:

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    // ...前面的代碼省略的,如果大家想回憶下,可以跳轉到2.1節
    if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
        getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
        // If somebody else (some other process) changed the prefs
        // file behind our back, we reload it.  This has been the
        // historical (if undocumented) behavior.
        // ->>> 重點1
        sp.startReloadIfChangedUnexpectedly();
    }
    return sp;
}

/**
 * 重點1 SharedPreferencesImpl # startReloadIfChangedUnexpectedly()
 */
void startReloadIfChangedUnexpectedly() {
    synchronized (mLock) {
        // TODO: wait for any pending writes to disk?
        // ->>> 重點2
        if (!hasFileChangedUnexpectedly()) {
            return;
        }
        // ->>> 重點3
        startLoadFromDisk();
    }
}

/**
 * 重點2 SharedPreferencesImpl # hasFileChangedUnexpectedly()
 *  如果文件發生了預期之外的修改,也就是說有其他進程在修改,就返回true,否則false
 */
private boolean hasFileChangedUnexpectedly() {
    synchronized (mLock) {
        // 如果mDiskWritesInFlight大於0,就證明是在當前進程中修改的,那就不用重新讀取
        if (mDiskWritesInFlight > 0) {
            // If we know we caused it, it's not unexpected.
            if (DEBUG) Log.d(TAG, "disk write in flight, not unexpected.");
            return false;
        }
    }

    final StructStat stat;
    try {
        /*
         * Metadata operations don't usually count as a block guard
         * violation, but we explicitly want this one.
         */
        BlockGuard.getThreadPolicy().onReadFromDisk();
        stat = Os.stat(mFile.getPath());
    } catch (ErrnoException e) {
        return true;
    }

    synchronized (mLock) {
        return !stat.st_mtim.equals(mStatTimestamp) || mStatSize != stat.st_size;
    }
}

/**
 * 重點3 SharedPreferencesImpl # startLoadFromDisk()
 */
private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            // ->>> 重點4,這塊代碼可以回到2.2.3看一下
            loadFromDisk();
        }
    }.start();
}

我們可以看到:每次獲取SharedPreferences實例的時候嘗試從磁盤中加載數據,並且是在異步線程中,因此一個線程的修改最終會反映到另一個線程,但不能立即反映到另一個進程,所以通過SharedPreferences無法實現多進程同步。

loadFromDisk()方法中我們最需要關注的是這一段:

// 如果備份文件已經存在,那就刪除源文件,並將備份文件替換爲源文件
if (mBackupFile.exists()) {
    mFile.delete();
    mBackupFile.renameTo(mFile);
}

這塊判斷了mBackupFile是否存在,那mBackupFile我們是在哪創建的呢?
整個SharedPreferencesImpl中有兩處:

  • 構造方法:會調用makeBackupFile()給傳入的file構造一個mBackupFile
  • writeToFile():在寫入到磁盤的文件時,如果沒有mBackupFile,就會根據當前的mFile重命名爲mBackupFile

writeToFile()enqueueDiskWrite()中被調用,這個方法太長了,我截取下關鍵信息:

@GuardedBy("mWritingToDiskLock")
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
    // ...
    boolean fileExists = mFile.exists();
    // ...
    
    // Rename the current file so it may be used as a backup during the next read
    if (fileExists) {
        // ...
        boolean backupFileExists = mBackupFile.exists();
        // ...
        if (!backupFileExists) {
            if (!mFile.renameTo(mBackupFile)) {
                Log.e(TAG, "Couldn't rename file " + mFile
                      + " to backup file " + mBackupFile);
                mcr.setDiskWriteResult(false, false);
                return;
            }
        } else {
            mFile.delete();
        }
    }

    // Attempt to write the file, delete the backup and return true as atomically as
    // possible.  If any exception occurs, delete the new file; next time we will restore
    // from the backup.
    try {
        FileOutputStream str = createFileOutputStream(mFile);
        // ...
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);

        writeTime = System.currentTimeMillis();

        FileUtils.sync(str);

        fsyncTime = System.currentTimeMillis();

        str.close();
        ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);

        // ...

        try {
            final StructStat stat = Os.stat(mFile.getPath());
            synchronized (mLock) {
                mStatTimestamp = stat.st_mtim;
                mStatSize = stat.st_size;
            }
        } catch (ErrnoException e) {
            // Do nothing
        }

        if (DEBUG) {
            fstatTime = System.currentTimeMillis();
        }

        // Writing was successful, delete the backup file if there is one.
        mBackupFile.delete();
        
        // ...

所以我們大致總結下這個方法的功能:

  • 如果源文件mFIle存在並且備份文件mBackupFile不存在,就將源文件重命名爲備份文件,如果源文件存在並且備份文件存在,就刪除源文件
  • 重新創建源文件mFile,並將內容寫進去
  • 刪除mBackupFile

結合一下loadFromDisk()writeToFile()兩個方法,我們可以推測出:當存在兩個進程,一個讀進程,一個寫進程,由於只有在創建SharedPreferencesImpl的時候創建了一個備份進程,此時讀進程會將源文件刪除,並將備份文件重命名爲源文件,這樣的結果就是,讀進程永遠只會看到寫之前的內容。並且由於寫文件需要調用createFileOutputStream(mFile),但是這個時候由於源文件被讀進程刪除了,所以導致寫進程的mFIle沒有了引用,也就會創建失敗,導致修改的數據無法更新到文件上,進而導致數據丟失。

3.3 建議優化

  • 不要在SP中存儲較大的key或者value
  • 只是用MODE_PRIVATE模式,其它模式都不要使用(也被棄用了)
  • 可以的話,儘量獲取一次Editor然後提交所有的數據
  • 不要高頻使用apply,因爲他每次都會新建一個線程;使用commit的時需謹慎,因爲他在主線程中操作(對,就是主線程,主線程並不是只能更新UI,但是還是就把主線程當做更新UI的爲好,我們的耗時操作最好不要在主線程中)
  • 如果需要在多進程中存儲數據,建議使用ContentProvider
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章