Android每週一輪子:SharedPreferences

前言

距離上一期的每週一輪子已通過去了好久了,離開的這段時間,去創業作了產品經理的工做,而後項目都失敗了,如今重啓開始新的技術之路,前段時間在面試,因此對於基礎知識點進行了從新的整理,因此結合着面試的內容,將對Android中的第三方框架還有FrameWork層的內容進行更系統的一個整理。開始第一篇,準備先從一個簡單的入手,咱們最多見的Android中的最多見的一種數據持久化方式,SharedPreferenced,經過SharePreference咱們能夠以鍵值對的形式來進行數據的存取。按照常規書寫慣例先從寫法入手。android

面經快速進入通道 快手,字節跳動,百度,美團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

只能夠被當前建立的應用讀取或者共享userid的應用bash

  • MODE_WORLD_READABLE

其它應用能夠進行讀多線程

  • MODE_WORLD_WRITEABLE

其它應用能夠讀寫app

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

安裝在設備中的每個Android包文件(.apk)都會被分配到一個屬於本身的統一的Linux用戶ID,而且爲它建立一個沙箱,以防止影響其餘應用程序(或者其餘應用程序影響它)。用戶ID 在應用程序安裝到設備中時被分配,而且在這個設備中保持它的永久性。經過Shared User id,擁有同一個User id的多個APK能夠配置成運行在同一個進程中.因此默認就是能夠互相訪問任意數據. 也能夠配置成運行成不一樣的進程,同時能夠訪問其餘APK的數據目錄下的數據庫和文件.就像訪問本程序的數據同樣.ide

怎麼讀?

在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的時候,咱們建立一個,而後從中根據name來獲取值,得不到值的時候,調用getSharedPreferencesPath來建立File,而後將其存入到SharedPrefsPaths之中,最後再調用getSharedPreferences根據File和Mode來獲取SharePreference

SharePrefsPaths是一個ArrayMap經過name做爲key,經過File做爲value,當找不到File的時候,就會根據name在指定的文件夾下建立一個名爲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中查找不到的時候,則會從新建立。整個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調用時,將在當前線程執行,當調用apply的時候則會在在一個子線程的HandlerThread中執行。

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能夠用來進行多個任務的執行等待,若是有一個任務想在三個任務執行完成以後再執行,那麼就能夠經過CountDownLatch來進行。既然apply是經過一個獨立的線程來執行的,那麼它會不會阻塞主線程呢?答案是會的,在QueueWork中的waitToFinish方法,該方法會在Activity的onPause的時候被調用,會將其中隊列的任務所有執行完成。所以其也是會阻塞主線程的執行。

磁盤文件加載與寫入

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

synchronized (mLock) {
    awaitLoadedLocked();
}

return new EditorImpl();
複製代碼

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

對於本地磁盤文件的操做上,爲了防止在寫的過程當中發生異常,因此在寫入的時候,會先將當前文件作一個備份,而後再進行寫操做,若是寫成功了,則將備份文件刪除,當下次進行讀寫的時候若是判斷到有備份文件,則能夠認爲上次文件的寫入是失敗的,讀取數據的時候則從備份文件中讀取,而後將備份文件重命名。這裏在文件操做上藉助與備份文件作轉化防止數據寫入出錯的設計仍是挺巧妙的。

問題?

SharePreference支持多線程嗎?

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

SharePreference支持多進程嗎?

SharePreference是不支持多進程的,由於對於磁盤中數據的加載只會進行一次,所以當一個進程對數據進行修改以後,是沒法體如今另外一個進程之中的。

SharePreference中的Mode是如何生效的?

在數據寫入完成,經過FileUtils的setPermission來根據Mode爲當前文件設置權限,而後在文件讀的時候也會進行相應的判斷,對於文件的讀寫權限將會根據寫入時寫入的mode做爲判斷依據。

總結

經過對於SharedPreferences的分析,能夠看出其大體實現上爲在本地磁盤經過xml的方式進行文件的存儲,當咱們獲取一個SharedPreferences的實例的時候,開啓線程將本地磁盤的數據讀取到內存之中,經過一個Map來存放,而後對其修改的時候,會建立一個新的Map來進行當前修改數據的存取,調用commit和apply的時候,將修改的數據寫回內存還有磁盤,同時根據設置的MODE,進行相應的判斷。

相關文章
相關標籤/搜索