Android源碼之SharedPreferences

0. 前言

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

這個存儲框架是很是輕量級的,若是咱們須要存一些小數據或者是一個小型的可序列化的Bean實體類的,使用SharedPreferences是最明智的選擇。android

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) 複製代碼

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

  • 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對象去使用。ide

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
相關文章
相關標籤/搜索