Android SharedPreferences 全面分析

我們經(jīng)常用SharedPreferences用來(lái)存儲(chǔ)一些比較小的鍵值對(duì)集合,適合保存應(yīng)用的配置參數(shù), 我們將會(huì)帶著以下幾個(gè)問(wèn)題來(lái)分析SharedPreferences的源碼實(shí)現(xiàn):

  • 數(shù)據(jù)是如何保存到磁盤(pán)的
  • commit() 和apply()的區(qū)別
  • 為什么會(huì)造成ANR
  • SharedPreferences有哪些缺點(diǎn)

源碼分析

本文參照Android-26的源碼,并不介紹SharedPreferences的基礎(chǔ)使用,而是從源碼角度來(lái)分析它的原理

獲取SharedPreferences

我們通過(guò)以下方法來(lái)獲取SharedPreferences實(shí)例

  1. context.getSharedPreferences
  2. 在Activity中g(shù)etSharedPreferences
  3. PreferenceManager.getDefaultSharedPreferences
    這三種方法最終都會(huì)調(diào)用到 ContextImpl.getSharedPreferences
  @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        //SharedPreferences對(duì)應(yīng)的xml文件,數(shù)據(jù)保存在其中
        File file;
        synchronized (ContextImpl.class) {
            ...//省略
            file = mSharedPrefsPaths.get(name);
            if (file == null) { 
                //如果沒(méi)有該name命名的文件,則新建一個(gè)并放入緩存
                file = getSharedPreferencesPath(name);
                mSharedPrefsPaths.put(name, file);
            }
        }
        return getSharedPreferences(file, mode);
    }

 @Override
    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) {
                sp = new SharedPreferencesImpl(file, mode);
                cache.put(file, sp);
                return sp;
            }
        }
       //mode設(shè)置為多進(jìn)程模式時(shí)會(huì)檢測(cè)SP文件最后修改的時(shí)間和大小,如果文件被其他進(jìn)程改變時(shí),則會(huì)重新加載
        if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
            getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
            sp.startReloadIfChangedUnexpectedly();
        }
        return sp;
    }

可以看到最終返回的是一個(gè)SharedPreferencesImpl對(duì)象,首先getSharedPreferencesCacheLocked()從一個(gè)靜態(tài)的ArrayMap中獲取SharedPreferences 緩存,如果有緩存中有SharedPreferencesImpl對(duì)象則返回,沒(méi)有的話則創(chuàng)建一個(gè)并存入緩存中,同時(shí)synchronized 包裹可以保證多線程同步,由此可見(jiàn)無(wú)論getSharedPreferences調(diào)用多少次,返回的都是一個(gè)SharedPreferencesImpl對(duì)象

SharedPreferencesImpl

SharedPreferencesImpl 實(shí)現(xiàn)了SharedPreferences這個(gè)接口,是我們通過(guò)getSharedPreferences得到的實(shí)體對(duì)象,所有存取操作都由該類(lèi)來(lái)實(shí)現(xiàn)

SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file); //備份文件
        mMode = mode;
        mLoaded = false;
        mMap = null; 
        startLoadFromDisk();
    }

mBackupFile 代表發(fā)生異常時(shí), 可通過(guò)備份文件來(lái)恢復(fù)數(shù)據(jù).
mLoaded 表示是否已經(jīng)將mFile中的數(shù)據(jù)都讀取到mMap 中
mMap 用于在內(nèi)存中緩存我們的配置數(shù)據(jù), 也就是 getXxx 數(shù)據(jù)的來(lái)源
startLoadFromDisk()從方法名即可看出是從硬盤(pán)中讀取數(shù)據(jù),看一下源碼

 private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }
 private void loadFromDisk() {
        synchronized (mLock) {
            if (mLoaded) {
                return;
            }
            if (mBackupFile.exists()) {
                mFile.delete();
                mBackupFile.renameTo(mFile);
            }
        }
        //...省略
        Map map = null;
        BufferedInputStream    str = new BufferedInputStream(new FileInputStream(mFile), 16*1024);
        map = XmlUtils.readMapXml(str);
       //...省略
        synchronized (mLock) {
            mLoaded = true;
            if (map != null) {
                mMap = map;
            } else {
                mMap = new HashMap<>();
            }
            mLock.notifyAll();
        }
    }

開(kāi)啟一個(gè)子線程來(lái)從硬盤(pán)讀取數(shù)據(jù),如果備份文件存在則直接使用災(zāi)備文件回滾,使用XmlUtils把文件所有的數(shù)據(jù)讀取到內(nèi)存中的mMap中,mLoaded = true 標(biāo)志SharedPreferencesImpl已經(jīng)將數(shù)據(jù)讀取完成,notifyAll()喚醒getXXX系列方法等待狀態(tài)的線程,由于已經(jīng)將數(shù)據(jù)中磁盤(pán)讀取到內(nèi)存中,此時(shí)調(diào)用getXXX系列方法就可以獲取值了

getString分析

 public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

synchronized 關(guān)鍵字保證了線程安全,然后直接從mMap中獲取對(duì)應(yīng)的鍵值對(duì)就可以了,當(dāng)我們調(diào)用getSharedPreferences 之后馬上調(diào)用getString方法有可能SharedPreferencesImpl在子線程中還沒(méi)有將文件中的數(shù)據(jù)讀取完,此時(shí)mMap 還沒(méi)有被賦值,所以awaitLoadedLocked()將會(huì)阻塞當(dāng)前線程,直到讀取完畢

private void awaitLoadedLocked() {
        while (!mLoaded) {
            try {
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
    }

mLoaded為false表示尚未讀取完成,其他的getXXX系列方法和getString如出一轍,都是先等待文件讀取完畢,然后從mMap中獲取相應(yīng)的value

數(shù)據(jù)保存

我們通過(guò)getSharedPreferences().edit()來(lái)put各種值,看一下.edit()獲取的是一個(gè)什么對(duì)象

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

保證磁盤(pán)讀取完畢后,返回了一個(gè)新的EditorImpl對(duì)象

  public final class EditorImpl implements Editor {
        private final Object mLock = new Object();
        private final Map<String, Object> mModified = Maps.newHashMap();
        private boolean mClear = false;

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

     public Editor remove(String key) {
            synchronized (mLock) {
                mModified.put(key, this);
                return this;
            }
        }
      ...//省略
}

EditorImpl 中有兩個(gè)重要屬性,mModified 用來(lái)暫時(shí)保存put方法提供的值,當(dāng)調(diào)用commit()或者apply()才會(huì)將mModified中的數(shù)據(jù)存儲(chǔ)到mMap,進(jìn)而保存到磁盤(pán)中,mClear標(biāo)志是否要清空文件中所有數(shù)據(jù)。接下來(lái)需要注意看remove()方法,調(diào)用getSharedPreferences().edit().remove()時(shí)是將當(dāng)前key的value置為this,刪除數(shù)據(jù)時(shí)檢測(cè)到value為this即可刪除
總結(jié):調(diào)用put()后,數(shù)據(jù)只是暫存到了EditorImpl 的mModified** 對(duì)象中,并沒(méi)有回寫(xiě)到磁盤(pán),調(diào)用commit()apply才會(huì)將數(shù)據(jù)寫(xiě)到磁盤(pán)中**

commit()

public boolean commit() {
           ...//省略
            MemoryCommitResult mcr = commitToMemory();
            SharedPreferencesImpl.this.enqueueDiskWrite(
                mcr, null /* sync write on this thread okay */);
            try {
                mcr.writtenToDiskLatch.await();
            } catch (InterruptedException e) {
                return false;
            } 
            return mcr.writeToDiskResult;
        }

主要有三步

  • commitToMemorymModified 中的數(shù)據(jù)寫(xiě)到內(nèi)存mMap
  • SharedPreferencesImpl.this.enqueueDiskWrite 將內(nèi)存中mMap的數(shù)據(jù)回寫(xiě)到磁盤(pán)中
  • mcr.writtenToDiskLatch.await() 線程等待,直到回寫(xiě)磁盤(pán)完畢
  1. commitToMemory()
    我們逐個(gè)分析,首先分析commitToMemory()返回一個(gè)MemoryCommitResult對(duì)象,代表了提交到內(nèi)存的返回結(jié)果
 private static class MemoryCommitResult {
           //...省略代碼
        final Map<String, Object> mapToWriteToDisk;
        //此處初始換CountDownLatch 的計(jì)數(shù)器為1
        final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);

        volatile boolean writeToDiskResult = false;
        boolean wasWritten = false;

        void setDiskWriteResult(boolean wasWritten, boolean result) {
            this.wasWritten = wasWritten;
            writeToDiskResult = result;
            writtenToDiskLatch.countDown();
        }
    }

其中關(guān)鍵有 writtenToDiskLatch 是一個(gè) CountDownLatch 對(duì)象,它允許一個(gè)或多個(gè)線程一直等待,直到回寫(xiě)磁盤(pán)線程的操作執(zhí)行完后再執(zhí)行,mapToWriteToDisk引用內(nèi)存中的mMap,writeToDiskResult代表回寫(xiě)磁盤(pán)是否成功,接下來(lái)繼續(xù)分析commitToMemory()

private MemoryCommitResult commitToMemory() {
            Map<String, Object> mapToWriteToDisk;
            synchronized (SharedPreferencesImpl.this.mLock) {
                mapToWriteToDisk = mMap;
                //需要寫(xiě)入磁盤(pán)次數(shù)+1
                mDiskWritesInFlight++;
                synchronized (mLock) {
                    if (mClear) {
                        //...省略代碼,
                        //如果調(diào)用了edit().clear()則清空內(nèi)存中的數(shù)據(jù)
                        mMap.clear();
                        mClear = false;
                    }
                    
                    //將putXXX()的數(shù)據(jù)提交到內(nèi)存中
                    for (Map.Entry<String, Object> e : mModified.entrySet()) {
                        String k = e.getKey();
                        Object v = e.getValue();
                        //value為this則刪除,與之前的getSharePreferences().edit().remove()對(duì)應(yīng)
                        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);
                        }
                    }
                    mModified.clear();
                }
            }
            return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                    mapToWriteToDisk);
        }

mDiskWritesInFlight代表寫(xiě)入磁盤(pán)這個(gè)操作的次數(shù),也是由synchronized 保證線程安全,首先判斷是否需要clear,如果需要這把mMap中的數(shù)據(jù)清空,需要注意此時(shí)mModified中數(shù)據(jù)還沒(méi)有復(fù)制到mMap中,所以以下代碼并不能將"foo" clear掉

sharedPreferences.edit()
        .putBoolean("foo";, true)        // foo 無(wú)法被 clear 掉
        .clear()
        .putBoolean("bar", true)
        .commit()

然后通過(guò)for循環(huán)將put到mModified中的數(shù)據(jù)添加到mMap中,mModified.clear()之后返回MemoryCommitResult
總結(jié)commitToMemory()只是將數(shù)據(jù)都寫(xiě)入到內(nèi)存中

  1. SharedPreferencesImpl.this.enqueueDiskWrite
private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        final boolean isFromSyncCommit = (postWriteRunnable == null);

        final Runnable writeToDiskRunnable = new Runnable() {
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (mLock) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                writeToDiskRunnable.run();
                return;
            }
        }
        //異步執(zhí)行任務(wù)
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }

commit() 時(shí)postWriteRunnable參數(shù)為null,所以isFromSyncCommit == true,進(jìn)入到if (isFromSyncCommit) 語(yǔ)句中,如果此時(shí)只有一個(gè)commit()操作,則直接在當(dāng)前線程執(zhí)行writeToFile()將內(nèi)存中的數(shù)據(jù)回寫(xiě)到磁盤(pán)中,如果此時(shí)有多個(gè)commit()則,排隊(duì)進(jìn)入QueuedWork中等待執(zhí)行,看一下writeToFile()的實(shí)現(xiàn)

private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
        //...省略
        boolean fileExists = mFile.exists();
        boolean backupFileExists = mBackupFile.exists();
            if (!backupFileExists) {
                if (!mFile.renameTo(mBackupFile)) {
                    mcr.setDiskWriteResult(false, false);
                    return;
                }
            } else {
                mFile.delete();
            }
        }
        try {
            FileOutputStream str = createFileOutputStream(mFile);
            if (str == null) {
                mcr.setDiskWriteResult(false, false);
                return;
            }
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
            str.close();
            mBackupFile.delete();
            mcr.setDiskWriteResult(true, true);
            return;
        } catch (Exception e) {
        }
        //如果寫(xiě)入操作出現(xiàn)異常,則將半成品刪掉
        if (mFile.exists()) {
            if (!mFile.delete()) {
                Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
            }
        }
        mcr.setDiskWriteResult(false, false);
    }
  • 將之前的配置文件mFile備份為buckup文件,然后刪除
  • mcr.mapToWriteToDisk即內(nèi)存中數(shù)據(jù),全部寫(xiě)入到新的mFile
  • 寫(xiě)入成功,刪掉備份文件,如果寫(xiě)入失敗則把半成品mFile刪掉
  1. mcr.writtenToDiskLatch.await()
    CountDownLatch.await()會(huì)阻塞當(dāng)前線程,直到CountDownLatch.countDown()使計(jì)數(shù)器值到達(dá)0時(shí),它表示磁盤(pán)寫(xiě)入線程已經(jīng)完成了任務(wù),然后在鎖上等待的線程就可以恢復(fù)執(zhí)行任務(wù)。在writeToFile()中,寫(xiě)入完成之后會(huì)調(diào)用mcr.setDiskWriteResult()中的writtenToDiskLatch.countDown()
 void setDiskWriteResult(boolean wasWritten, boolean result) {
            this.wasWritten = wasWritten;
            writeToDiskResult = result;
            writtenToDiskLatch.countDown();
        }

writtenToDiskLatch初始時(shí)計(jì)數(shù)器為1,countDown()之后為0,此時(shí)磁盤(pán)已經(jīng)回寫(xiě)完畢,commit()方法繼續(xù)執(zhí)行,返回結(jié)果
commit()總結(jié)

  • 流程是先寫(xiě)入內(nèi)存寫(xiě)入磁盤(pán)
  • 寫(xiě)入磁盤(pán)完成之前調(diào)用線程會(huì)一直等待,直到內(nèi)存和磁盤(pán)都已經(jīng)同步完畢
  • 每次寫(xiě)入磁盤(pán)時(shí)都會(huì)從內(nèi)存中將所有數(shù)據(jù)都全量寫(xiě)入,效率并不高

apply()

       public void apply() { 
            //第一步:提交到內(nèi)存
            final MemoryCommitResult mcr = commitToMemory();
            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");
                        }
                    }
                };
            //第二步:確保異步磁盤(pán)寫(xiě)入完畢
            QueuedWork.addFinisher(awaitCommit);

            Runnable postWriteRunnable = new Runnable() {
                    public void run() {
                        awaitCommit.run();
                        QueuedWork.removeFinisher(awaitCommit);
                    }
                };
            // 第三步:寫(xiě)入磁盤(pán)
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
        }
  • 第一步提交到內(nèi)存和commit()是一樣的
  • 第二步中將任務(wù)mcr.writtenToDiskLatch.await()提交到QueuedWork之中,該任務(wù)的作用是讓線程等待,而釋放的時(shí)機(jī)跟commit()一樣(詳細(xì)代碼看上述commit()),但是QueuedWork.addFinisher()將線程等待的任務(wù)提交之后并沒(méi)有立即運(yùn)行,而是保存在了一個(gè)隊(duì)列之中,當(dāng)應(yīng)用收到系統(tǒng)廣播,或者被調(diào)用 onPause 等一些時(shí)機(jī)才會(huì)運(yùn)行(詳情查看QueuedWork源碼,在ActivityThread中可以找到調(diào)用任務(wù)的方法waitToFinish())
  • 第三步同commit,不同點(diǎn)在于enqueueDiskWrite(mcr, postWriteRunnable)傳遞了Runnable,在異步線程中寫(xiě)入磁盤(pán)
    apply總結(jié)
  • 異步寫(xiě)入磁盤(pán),沒(méi)有等待結(jié)果,直接返回
  • 應(yīng)用收到系統(tǒng)廣播,或者被調(diào)用 onPause等時(shí)機(jī),如果磁盤(pán)寫(xiě)入未完成則主線程會(huì)等待其完成
  • commit()寫(xiě)入過(guò)程一樣,都是全量寫(xiě)入

SharedPreferences總結(jié)

通過(guò)上文對(duì)SharedPreferences分析,我們已經(jīng)可以對(duì)開(kāi)頭的幾個(gè)問(wèn)題進(jìn)行回答并總結(jié)了

  • 數(shù)據(jù)是如何保存到磁盤(pán)的
    答:通過(guò)putXXX系列方法將數(shù)據(jù)先保存到內(nèi)存中,調(diào)用commit()或者apply(之后將所有數(shù)據(jù)全量寫(xiě)入磁盤(pán)文件中
  • commit() 和apply()的區(qū)別
    答:commit()線程同步寫(xiě)入,寫(xiě)入完成時(shí)才會(huì)返回,如果在主線程調(diào)用,寫(xiě)入過(guò)程比較費(fèi)時(shí)可能會(huì)阻塞主線程,
    apply異步線程寫(xiě)入,但是應(yīng)用收到系統(tǒng)廣播,或者被調(diào)用 onPause等時(shí)機(jī),未完成寫(xiě)入任務(wù)時(shí)主線程會(huì)等待其完成
  • commit()和apply()相同點(diǎn)
    答:都是全量寫(xiě)入,如果SharedPreferences中數(shù)據(jù)量很多,則每次寫(xiě)入都會(huì)很慢
  • 為什么會(huì)造成ANR
    答:commit()和apply()都可能在成ANR,分析如上
  • SharedPreferences有哪些缺點(diǎn)
    答:1. 全量寫(xiě)入:commit() 還是 apply(),即使我們只改動(dòng)其中一條數(shù)據(jù),都會(huì)把整個(gè)數(shù)據(jù)寫(xiě)入到文件中
    2. 卡頓:commit() 還是 apply()都有可能造成ANR
    3. 跨進(jìn)程不安全:MODE_MULTI_PROCESS已被谷歌標(biāo)為Deprecated
    總之:系統(tǒng)提供的 SharedPreferences 的應(yīng)用場(chǎng)景是用來(lái)存儲(chǔ)一些簡(jiǎn)單、輕量的數(shù)據(jù),例如配置文件等,不適合json、html等,并且每個(gè)SharedPreference不宜過(guò)大,考慮將頻繁修改的配置項(xiàng)單獨(dú)隔離
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容