Android 輕量級緩存 SharedPreference 源碼解析

本文是獨立解析源碼的第二篇,SharedPreference 是一個 Android 開發自帶的適合保存輕量級數據的 K-V 存儲庫,它使用了 XML 的方式來存儲數據,比如我就經常用它保存一些如用戶登錄信息等輕量級數據。那么今天就讓我們來分析一下它的源碼,研究一下其內部實現。

獲取SharedPreferences

我們在使用 SharedPreferences 時首先是需要獲取到這個 SharedPreferences 的,因此我們首先從 SharedPreferences 的獲取入手,來分析其源碼。

根據名稱獲取 SP

不論是在 Activity 中調用 getPreferences() 方法還是調用 Context 的 getSharedPreferences 方法,最終都是調用到了 ContextImpl 的 getSharedPreferences(String name, int mode) 方法。我們先看看它的代碼:

@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.
    if (mPackageInfo.getApplicationInfo().targetSdkVersion <
            Build.VERSION_CODES.KITKAT) {
        if (name == null) {
            name = "null";
        }
    }
    File file;
    synchronized (ContextImpl.class) {
        if (mSharedPrefsPaths == null) {
            mSharedPrefsPaths = new ArrayMap<>();
        }
        file = mSharedPrefsPaths.get(name);
        if (file == null) {
            file = getSharedPreferencesPath(name);
            mSharedPrefsPaths.put(name, file);
        }
    }
    return getSharedPreferences(file, mode);
}

可以看到,它首先對 Android 4.4 以下的設備做了特殊處理,之后將對 mSharedPrefsPaths 的操作加了鎖。mSharedPrefsPaths 的聲明如下:

private ArrayMap<String, File> mSharedPrefsPaths;

可以看到它是一個以 name 為 key,name 對應的 File 為 value 的 HashMap。首先調用了 getSharedPreferencesPath 方法構建出了 name 對應的 File,將其放入 map 后再調用了 getSharedPreferences(File file, int mode) 方法。

獲取 SP 名稱對應的 File 對象

我們先看看是如何構建出 name 對應的 File 的。

@Override
public File getSharedPreferencesPath(String name) {
    return makeFilename(getPreferencesDir(), name + ".xml");
}

可以看到,調用了 makeFilename 方法來創建一個名為 name.xml 的 File。makeFilename 中僅僅是做了一些判斷,之后 new 出了這個 File 對象并返回。

可以看到,SharedPreference 確實是使用 xml 來保存其中的 K-V 數據的,而具體存儲的路徑我們這里就不再關心了,有興趣的可以點進去看看。

根據創建的 File 對象獲取 SP

我們接著看到獲取到 File 并放入 Map 后調用的 getSharedPreferences(file, mode) 方法:

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();  // 1
        sp = cache.get(file);
        if (sp == null) {
            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");
                }
            }
            sp = new SharedPreferencesImpl(file, mode); // 2
            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 處,這里調用了 getSharedPreferencesCacheLocked 來獲取到了一個 ArrayMap<File, SharedPreferencesImpl>,之后再從這個 Map 中嘗試獲取到對應的 SharedPreferencesImpl 實現類(簡稱 SPI)。

之后看到注釋 2 處,當獲取不到對應 SPI 時,再創建一個對應的 SPI,并將其加入這個 ArrayMap 中。

這里很明顯是一個緩存機制的實現,以加快之后獲取 SP 的速度,同時可以發現,SP 其實只是一個接口,而 SPI 才是其具體的實現類。

緩存機制

那么我們先來看看其緩存機制,進入 getSharedPreferencesCacheLocked 方法:

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;
}

顯然,這里有個全局的 ArrayMap:sSharedPrefsCache。

它是一個 ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> 類型的 Map,而從代碼中可以看出,它是根據 PackageName 來保存不同的 SP 緩存 Map 的,通過這樣的方式,就保證了不同 PackageName 中相同 name 的 SP 從緩存中0拿到的數據是不同的。

SharedPreferencesImpl

那么終于到了我們 SPI 的創建了,在 cache 中找不到對應的 SPI 時,就會 new 出一個 SPI,看看它的構造函數:

SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file); // 1
    mMode = mode;
    mLoaded = false;
    mMap = null;
    startLoadFromDisk();    // 2
}

可以看到注釋 1 處它調用了 makeBackupFile 來進行備份文件的創建。

之后在注釋 2 處則調用了 startLoadFromDisk 來開始從 Disk 載入信息。

首先我們看看 makeBackupFile 方法:

static File makeBackupFile(File prefsFile) {
    return new File(prefsFile.getPath() + ".bak");
}

很簡單,返回了一個同目錄下的后綴名為 .bak 的同名文件對象。

從 Disk 加載數據

接著,我們看看 startLoadFromDisk 方法:

private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}

可以看到,首先在加鎖的情況下對 mLoaded 進行了修改,之后則開了個名為「SharedPreferencesImpl-load」的線程來加載數據。

我們看到 loadFromDisk 方法:

private void loadFromDisk() {
    synchronized (mLock) {  // 1
        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 = null;
    StructStat stat = null;
    try { // 2
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16*1024);
                map = XmlUtils.readMapXml(str);
            } catch (Exception e) {
                Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
            } finally {
                IoUtils.closeQuietly(str);
            }
        }
    } catch (ErrnoException e) {
        /* ignore */
    }
    synchronized (mLock) {  // 3
        mLoaded = true;
        if (map != null) {
            mMap = map;
            mStatTimestamp = stat.st_mtim;
            mStatSize = stat.st_size;
        } else {
            mMap = new HashMap<>();
        }
        mLock.notifyAll();
    }
}

代碼比較長,我們慢慢分析

首先在注釋 1 處,如果已經加載過,則不再進行加載,之后又開始判斷是否存在備份文件,若存在則直接將備份文件直接修改為數據文件 ${name}.xml。

之后在注釋 2 處,通過 XmlUtils 將 xml 中的數據讀取為一個 Map。由于本文主要是對 SP 的大致流程的解讀,因此關于 XML 文件的具體讀取部分,有興趣的讀者可以自己進入源碼研究。

之后在注釋 3 處,進行了一些收尾處理,將 mLoaded 置為 true,并對 mMap 進行了判空處理,以保證在 xml 沒有數據的情況下其仍不為 null,最后釋放了這個讀取的鎖,表示讀取成功。

編輯 SharedPreferences

我們都知道,真正對 SP 的操作其實都是在 Editor 中的,它其實是一個接口,具體的實現類為 EditorImpl。讓我們先看看 Editor 的獲取:

獲取 Editor

看到 SharedPreferencesImpl 的 edit 方法:

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) {
        awaitLoadedLocked();
    }
    return new EditorImpl();
}

可以看到,這里首先先調用了 awaitLoadedLocked() 方法來等待讀取的完成,當讀取完成后才會真正創建并返回 EditorImpl 對象。

等待讀取機制

由于讀取過程是一個異步的過程,很有可能導致讀取還沒結束,我們就開始編輯,因此這里用到了一個 awaitLoadedLocked 方法來阻塞線程,直到讀取過程完成,下面我們可以先看看 awaitLoadedLocked 方法:

private void awaitLoadedLocked() {
    if (!mLoaded) {
        // Raise an explicit StrictMode onReadFromDisk for th
        // 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) {
        }
    }
}

可以看到,這里會阻塞直到 mLoaded 為 true,這樣就保證了該方法后的方法都會在讀取操作進行后執行。

EditorImpl

前面我們提到了 Edit 的真正實現類是 EditorImpl,它其實是 SPI 的一個內部類。它內部維護了一個Map<String, Object>: mModified,通過 mModified 來存放對 SP 進行的操作,此時還不會提交到 SPI 中的 mMap,我們做的操作都是在改變 mModified。

下面列出一些 EditorImpl 對外提供的修改接口,其實都是在對 mModified 這個 Map 進行修改,具體代碼就不再講解,比較簡單:

public Editor putString(String key, @Nullable String val
    synchronized (mLock) {
        mModified.put(key, value);
        return this;
    }
}
public Editor putStringSet(String key, @Nullable Set<Str
    synchronized (mLock) {
        mModified.put(key,
                (values == null) ? null : new HashSet<St
        return this;
    }
}
public Editor putInt(String key, int value) {
    synchronized (mLock) {
        mModified.put(key, value);
        return this;
    }
}
public Editor putLong(String key, long value) {
    synchronized (mLock) {
        mModified.put(key, value);
        return this;
    }
}
public Editor putFloat(String key, float value) {
    synchronized (mLock) {
        mModified.put(key, value);
        return this;
    }
}
public Editor putBoolean(String key, boolean value) {
    synchronized (mLock) {
        mModified.put(key, value);
        return this;
    }
}
public Editor remove(String key) {
    synchronized (mLock) {
        mModified.put(key, this);
        return this;
    }
}
public Editor clear() {
    synchronized (mLock) {
        mClear = true;
        return this;
    }
}

提交 SharedPreferences

提交本來可以放到編輯中的,但因為它才是重頭戲,因此我們單獨拎出來講一下。

我們都知道 SP 的提交有兩種方式——apply 和 commit。下面我們來分別分析:

apply

public void apply() {
    final long startTime = System.currentTimeMillis();
    final MemoryCommitResult mcr = commitToMemory();    // 1
    final Runnable awaitCommit = new Runnable() {
            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");
                }
            }
        };
    QueuedWork.addFinisher(awaitCommit);
    Runnable postWriteRunnable = new Runnable() {
            public void run() {
                awaitCommit.run();
                QueuedWork.removeFinisher(awaitCommit);
            }
        };
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);    // 2
    // 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);
}

首先看到注釋 1 處,調用了 commitToMemory 方法,它內部就是將原先讀取進來的 mMap 與剛剛修改過的 mModified 進行合并,并存儲于返回的 MemoryCommitResult mcr中

而在注釋 2 處,調用了 enqueueDiskWrite 方法,傳入了之前構造的 Runnable 對象,這里的目的是進行一個異步的寫操作。

前面提到的兩個方法,我們放到后面分析。

也就是說,apply 方法會將數據先提交到內存,再開啟一個異步過程來將數據寫入硬盤

commit

public boolean commit() {
    long startTime = 0;
    if (DEBUG) {
        startTime = System.currentTimeMillis();
    }
    MemoryCommitResult mcr = commitToMemory();  // 1
    SharedPreferencesImpl.this.enqueueDiskWrite(    // 2
        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");
        }
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}

看到注釋 1 處,可以發現,同樣調用了 commitToMemory 方法進行了合并。

之后看到 2 處,同樣調用了 enqueueDiskWrite 方法,不過傳入的第二個不再是 Runnable 方法。這里提一下,如果 enqueueDiskWrite 方法傳入的第二個參數為 null,則會在當前線程執行寫入操作。

也就是說,commit 方法會將數據先提交到內存,但之后則是一個同步的過程寫入硬盤

同步數據至內存

下面我們來看看兩個方法中都調用了的 commitToMemory 的具體實現:

private MemoryCommitResult commitToMemory() {
        ...
        synchronized (mLock) {
            boolean changesMade = false;
            if (mClear) {
                if (!mMap.isEmpty()) {
                    changesMade = true;
                    mMap.clear();
                }
                mClear = false;
            }
            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;
                        }
                    }
                    mMap.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 的數據與 mModified 的數據進行了整合,之后將 mModified 重新清空。最后將合并的數據放入了 MemoryCommitResult 中。

寫入數據至硬盤

我們同樣看到 apply 和 commit 都調用了的方法 enqueueDiskWrite:

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
    final boolean isFromSyncCommit = (postWriteRunnable == null);   // 1
    final Runnable writeToDiskRunnable = new Runnable() {
            public void run() {
                synchronized (mWritingToDiskLock) {
                    writeToFile(mcr, isFromSyncCommit);
                }
                synchronized (mLock) {
                    mDiskWritesInFlight--;
                }
                if (postWriteRunnable != null) {
                    postWriteRunnable.run();
                }
            }
        };
    // Typical #commit() path with fewer allocations, doing a write on
    // the current thread.
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();  // 2 
            return;
        }
    }
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

可以看到 1 處,若第二個 Runnable 為 null 的話,則會將 isFromSyncCommit 置為 true,也就是寫入會是一個同步的過程。之后在注釋 2 處便進行了同步寫入。否則會構造一個 Runnable 來提供給 QueueWork 進行異步寫入。

QueueWork 類內部維護了一個 single 線程池,這樣可以達到我們異步寫入的目的。

而 writeToFile 方法中其實就是又調用了之前的 XmlUtils 來進行 XML 的寫入。

總結

SharedPreferences 其實就是一個用使用 XML 進行保存的 K-V 存儲庫。

在獲取 SP 時會進行數據的加載,將 name 對應的 xml 文件以 Map 的形式讀入到內存。

而 SP 的編輯操作其實都是在 Editor 內實現,它內部維護了一個新的 Map,所有的編輯操作其實都是在操作這個 Map,只有提交時才會與之前讀取的數據進行合并。

其提交分為兩種,apply 和 commit,它們的特性如下

  • apply

    • 會將數據先提交到內存,再開啟一個異步過程來將數據寫入硬盤
    • 返回值時可能寫入操作還沒有結束
    • 寫入失敗時不會有任何提示
  • commit

    • 會將數據先提交到內存,但之后則是一個同步的過程寫入硬盤

    • 寫入操作結束后才會返回值

    • 寫入失敗會有提示

因此,當我們對寫入的結果不那么關心的情況下,可以使用 apply 進行異步寫入,而當我們對寫入結果十分關心且提交后有后續操作的話最好使用 commit 來進行同步寫入。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容