Android每周一輪子:SharedPreferences

前言

距離上一期的每周一輪子已經過去了很久了,離開的這段時間,去創(chuàng)業(yè)做了產品經理的工作,然后項目都失敗了,現在重啟開始新的技術之路,前段時間在面試,所以對于基礎知識點進行了重新的整理,所以結合著面試的內容,將對Android中的第三方框架還有FrameWork層的內容進行更系統的一個整理。開始第一篇,準備先從一個簡單的入手,我們最常見的Android中的最常見的一種數據持久化方式,SharedPreferenced,通過SharePreference我們可以以鍵值對的形式來進行數據的存取。按照常規(guī)書寫慣例先從寫法入手。

面經快速進入通道
快手,字節(jié)跳動,百度,美團Offer之旅(Android面經分享)

基礎使用

SharedPreferences preferences = getSharedPreferences("name", MODE_PRIVATE);
preferences.getString("name", "");
preferences.edit().putString("name", null).apply();
preferences.edit().putString("name", null).commit();

上述是SharedPreferences的一個實現方式,通過指定名稱和類型來獲取一個SharedPreference,對于類型后面會展開來講,然后通過get方法可以根據鍵值來獲取相應存取的值,對于數據的寫入,通過edit方法后調用相應數據類型的put方法來添加數據,最后調用apply和commit方法來提交數據。那么接下來,我們來跟進一下看SharedPreferences是如何實現讀寫操作的,還有不同的類型的SharedPreference的差異性在哪里。

SharedPreferences實現

下面是SharedPreferences支持的MODE

  • MODE_PRIVATE

只可以被當前創(chuàng)建的應用讀取或者共享userid的應用

  • MODE_WORLD_READABLE

其它應用可以進行讀

  • MODE_WORLD_WRITEABLE

其它應用可以讀寫

上述是SharedPreferences實現的常見三種MODE

安裝在設備中的每一個Android包文件(.apk)都會被分配到一個屬于自己的統一的Linux用戶ID,并且為它創(chuàng)建一個沙箱,以防止影響其他應用程序(或者其他應用程序影響它)。用戶ID 在應用程序安裝到設備中時被分配,并且在這個設備中保持它的永久性。通過Shared User id,擁有同一個User id的多個APK可以配置成運行在同一個進程中.所以默認就是可以互相訪問任意數據. 也可以配置成運行成不同的進程,同時可以訪問其他APK的數據目錄下的數據庫和文件.就像訪問本程序的數據一樣.

怎么讀?

在context中,我們可以通過調用getSharePreferenced方法來獲取SharePreferences,那么我們先來看一下其具體實現。

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

首先根據名稱從SharedPrefsPaths中進行查找,SharedPrefsPaths是一個ArrayMap,通過我們傳遞的名稱作為鍵,磁盤存儲文件File作為值,當SharedPrefsPaths為null的時候,我們創(chuàng)建一個,然后從中根據name來獲取值,得不到值的時候,調用getSharedPreferencesPath來創(chuàng)建File,然后將其存入到SharedPrefsPaths之中,最后再調用getSharedPreferences根據File和Mode來獲取SharePreference

SharePrefsPaths是一個ArrayMap通過name作為key,通過File作為value,當找不到File的時候,就會根據name在指定的文件夾下創(chuàng)建一個名為name.xml的文件。然后將其緩存起來。

public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        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);
            cache.put(file, sp);
            return sp;
        }
    }
    ....
    return sp;
}

SharedPreferences的實際獲取是通過一個以File為鍵,以SharedPreferencesImpl為值的ArrayMap中存放的,當Cache中查找不到的時候,則會重新創(chuàng)建。整個SharedPreferences的核心實現就是在SharedPreferencesImpl之中。下面,我們來看一下SharedPreferencesImpl的具體實現。

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

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

在其構造函數中調用startLoadFromDisk來進行數據的加載,此處實現是通過新開線程來實現的。loadFromDisk的核心實現代碼如下。

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

通過對于xml的解析得到一個Map,后面就可以通過name對Map進行查詢即可得到相應的值。至此,我們已經知道了如何從SharedPreferences進行數據的讀取數值了,從本地磁盤讀取數值到內存之中的Map,我們查找的時候首先進行Map查找就可以了。當有修改的時候回寫到磁盤之中。那么接下來,我們來看一下數據應該如何寫回。

怎么寫?

對于SharedPreferenced值的寫入,這里我們先從commit開始。分析commit之前,我們先看一下edit是如何實現的。

public Editor edit() {
    synchronized (mLock) {
        awaitLoadedLocked();
    }
    return new EditorImpl();
}

首先當調用edit方法來進行寫操作的時候,會獲取到讀鎖,來等待讀操作完成,因為讀操作是在一個子線程中進行,因此需要通過await來進行等待,返回了一個EditorImpl實例,對于其中的相關修改操作,其內部有一個Map來存放要寫入和要修改的數據。后面我們調用commit和apply的時候,會先進行內存中數據的修改,然后再進行本地文件的修改。接下來,先看一下commit方法。

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

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

    MemoryCommitResult mcr = commitToMemory();

    SharedPreferencesImpl.this.enqueueDiskWrite(
        mcr, null /* sync write on this thread okay */);
    try {
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    } finally {
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}

commit方法中首先調用了commitToMemory方法,然后調用了enqueueDiskWrite方法來進行數據寫入到磁盤。

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);
        }
        mapToWriteToDisk = mMap;
        mDiskWritesInFlight++;

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

        synchronized (mEditorLock) {
            boolean changesMade = false;

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

commitToMemory的實現是將記錄的修改的Map,同步修改到最開始從本地加載數據Map中。enqueueDiskWrite方法的實現中核心在于一個runnable,runnable封裝了文件寫入的封裝,當commit調用時,將在當前線程執(zhí)行,當調用apply的時候則會在在一個子線程的HandlerThread中執(zhí)行。

final Runnable writeToDiskRunnable = new Runnable() {
        @Override
        public void run() {
            synchronized (mWritingToDiskLock) {
                writeToFile(mcr, isFromSyncCommit);
            }
            synchronized (mLock) {
                mDiskWritesInFlight--;
            }
            if (postWriteRunnable != null) {
                postWriteRunnable.run();
            }
        }
    };

apply的實現中首先將修改寫到內存之中,然后再寫入到磁盤,commit是在當前線程直接寫入,而對于apply則是通過一個HandlerThread來實現寫入。對于其中的排隊實現,通過的是CountDownLatch來實現的,可以對其進行await阻塞等待,當調用其countDown方法的時候,就會將其寫入到磁盤之中。CountDownLatch可以用來進行多個任務的執(zhí)行等待,如果有一個任務想在三個任務執(zhí)行完成之后再執(zhí)行,那么就可以通過CountDownLatch來進行。既然apply是通過一個獨立的線程來執(zhí)行的,那么它會不會阻塞主線程呢?答案是會的,在QueueWork中的waitToFinish方法,該方法會在Activity的onPause的時候被調用,會將其中隊列的任務全部執(zhí)行完成。因此其也是會阻塞主線程的執(zhí)行。

磁盤文件加載與寫入

分析完上面的SharedPreferences的讀寫過程,首先有一個疑問就是如果我們在進行本地文件向內存中裝載的時候,再進行文件的寫入應該怎么處理?

synchronized (mLock) {
    awaitLoadedLocked();
}

return new EditorImpl();

在返回EditorImpl實現的時候,首先調用了awaitLoadedLocked,通過該方法實現對于從本地磁盤讀鎖的等待。只有當本地的文件已經加載到內存之中,才會進行后面的相關寫操作。

對于本地磁盤文件的操作上,為了防止在寫的過程中發(fā)生異常,所以在寫入的時候,會先將當前文件做一個備份,然后再進行寫操作,如果寫成功了,則將備份文件刪除,當下次進行讀寫的時候如果判斷到有備份文件,則可以認為上次文件的寫入是失敗的,讀取數據的時候則從備份文件中讀取,然后將備份文件重命名。這里在文件操作上借助與備份文件做轉化防止數據寫入出錯的設計還是挺巧妙的。

問題

SharePreference支持多線程嗎?

SharePreference是支持進行多線程讀寫的,可以進行多線程下的讀寫操作,不會出現數據錯亂的問題,內部通過鎖來進行控制。對于同一個進程,其中只存在一個SharePreference實例。

SharePreference支持多進程嗎?

SharePreference是不支持多進程的,因為對于磁盤中數據的加載只會進行一次,因此當一個進程對數據進行修改之后,是無法體現在另一個進程之中的。

SharePreference中的Mode是如何生效的?

在數據寫入完成,通過FileUtils的setPermission來根據Mode為當前文件設置權限,然后在文件讀的時候也會進行相應的判斷,對于文件的讀寫權限將會根據寫入時寫入的mode作為判斷依據。

總結

通過對于SharedPreferences的分析,可以看出其大致實現上為在本地磁盤通過xml的方式進行文件的存儲,當我們獲取一個SharedPreferences的實例的時候,開啟線程將本地磁盤的數據讀取到內存之中,通過一個Map來存放,然后對其修改的時候,會創(chuàng)建一個新的Map來進行當前修改數據的存取,調用commit和apply的時候,將修改的數據寫回內存還有磁盤,同時根據設置的MODE,進行相應的判斷。

?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容