數據存儲之——SharedPreferences不要再誤用了!(Android Q)

SharedPreferences(簡稱sp)Android平臺上一個輕量級的存儲輔助類,它提供了key-value鍵值對的接口,用來保存應用的一些常用配置,在應用中通常做一些簡單數據的持久化緩存。本文將詳細的分析SharedPreferences的實現方式、存儲機制、如何正確使用它以及sp的性能問題等方面。

SharedPreferences實現詳解


我們在Android開發中,如果想要保存一個相對較小的鍵值對集合,則應使用SharedPreferences API。SharedPreferences對象指向包含鍵值對的文件,並提供讀寫這些鍵值對的簡單方法。SharedPreferences API提供了string,set,int,long,float,boolean六種數據類型的數據訪問接口。sp文件在存儲區最終數據是以xml形式進行存儲。

獲取SharedPreferences對象

想要使用sp來存取數據,我們首先要了解如何去獲取它,Android的Context類爲我們提供了獲取SharedPreferences對象的抽象接口。

Context對象的getSharedPreferences()方法可以獲取一個SharedPreferences對象,之後我們就可以通過SharedPreferences來管理我們的鍵值對數據了。

Context中的方法定義:

    public abstract SharedPreferences getSharedPreferences(File file, @PreferencesMode int mode);

context類爲我們提供了訪問sp的抽象接口,真正的而實現實在ContextImpl類中。

ContextImpl中的方法實現:

我們來看源碼實現:

    @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) {
                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;
            }
        }
        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;
    }
參數:
  • file參數用於指定SharedPreferences文件的名稱,如果指定的文件不存在則會創建一個,SharedPreferences文件都是存放在“/data/data/<包名>/shared_prefs/”目錄下。
  • mode參數用於指定操作模式,它的可選值有2種,MODE_PRIVATE(默認值,指定文件是私有的,只可被當前應用訪問或者相同user ID的進程)和MODE_MULTI_PROCESS(多進程共享模式)。
邏輯解析:
  1. getSharedPreferences首先會從內存緩存中查找是否已經存在我們想要獲取的SharedPreferences實例。
  2. 如果不存在,則會爲相應的file創建一個SharedPreferencesImpl實例,並且將它添加到緩存中。
  3. 判斷如果參數mode的值是Context.MODE_MULTI_PROCESS或者App的targetSdkVersion小於Android 3.0,則執行調用sp的startReloadIfChangedUnexpectedly方法執行sp文件的重新加載工作,我們稍後分析。

MODE_MULTI_PROCESS模式

我們通常調用getSharedPreferences方法時,使用默認模式即可,也是google推薦的方式。Context.MODE_MULTI_PROCESS是多線程共享模式,理論上可以做到多進程數據共享功能,但是,此功能已廢棄,不建議使用了。Context.MODE_MULTI_PROCESS模式是在Android 3.0之前版本的遺留功能,在之後某些版本的Android中無法可靠工作,而且也不提供任何協調跨進程併發修改的機制。我們不應該嘗試使用該模式,如果我們有跨進程數據傳輸的需求,應該使用明確的跨進行數據共享機制,例如ContentProvider等來實現。

另外,MODE_MULTI_PROCESS模式需要進程在每次訪問數據時都要進行io操作,以檢查數據是否被其他進程改變,這樣io操作造成了很大的性能消耗,如果我們在開發中誤使用了該模式,應立即改成默認模式!

sp數據文件從存儲分區加載到內存過程分析

SharedPreferences工具類爲我們提供了管理sp數據的接口,從而簡化了數據存取操作。sp數據文件最終是以.xml文件的格式,存儲到App數據私有目錄:/data/data/<app包名>/shared_prefs/目錄下,那麼sp文件是如何從存儲區加載到內存中的呢?

在Context的getSharedPreferences方法獲取SharedPreferences對象時,我們發現如果參數mode的值是Context.MODE_MULTI_PROCESS或者App的targetSdkVersion小於Android 3.0,則執行調用sp的startReloadIfChangedUnexpectedly方法執行sp文件的重新加載工作,這裏就涉及到了sp文件加載過程,同樣SharedPreferencesImpl類的構造過程也會涉及到sp文件的加載。

android.app.SharedPreferencesImpl:

    @UnsupportedAppUsage
    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);//這裏注意,創建了一個備份文件。
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = null;
        startLoadFromDisk(); //加載xml文件到內存中
    }

SharedPreferencesImpl類的startReloadIfChangedUnexpectedly方法:

    @UnsupportedAppUsage
    void startReloadIfChangedUnexpectedly() {
        synchronized (mLock) {
            // TODO: wait for any pending writes to disk?
            if (!hasFileChangedUnexpectedly()) {
                return;
            }
            startLoadFromDisk();
        }
    }

    

SharedPreferencesImpl的構造方法和startReloadIfChangedUnexpectedly都調用到了startLoadFromDisk()方法。

startLoadFromDisk()方法:

    @UnsupportedAppUsage
    private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }
邏輯解析:
  1. startLoadFromDisk方法中,先重置mLoaded狀態。
  2. 創建一個工作線程,線程名爲SharedPreferencesImpl-load。
  3. 線程調用loadFromDisk()方法執行sp文件從存儲分區到內存的加載工作。

loadFromDisk()方法:

    private void loadFromDisk() {
        synchronized (mLock) {
            if (mLoaded) { //判斷mLoaded狀態
                return;
            }
            if (mBackupFile.exists()) { //如果備份文件存在,則代表原始文件數據出現錯誤,使用備份文件,替換掉原始文件。
                mFile.delete();
                mBackupFile.renameTo(mFile);
            }
        }
        ……

        Map<String, Object> map = null;
        StructStat stat = null;
        Throwable thrown = null;
        try {
            stat = Os.stat(mFile.getPath()); //文件存在
            if (mFile.canRead()) { //文件可讀取
                BufferedInputStream str = null;
                try {
                    str = new BufferedInputStream(
                            new FileInputStream(mFile), 16 * 1024); //獲取輸入流
                    map = (Map<String, Object>) XmlUtils.readMapXml(str);//解析xml文件到map中
                } 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();
            }
        }
    }
邏輯解析:
  1. 判斷備份文件是否存在,如果存在則代表原始文件數據出現錯誤,使用備份文件,替換掉原始文件。
  2. 如果文件存在並且可讀取,則把字節流讀取到內存中,並且使用XmlUtils.readMapXml工具方法對原始數據進行解析。
  3. 數據解析後得到一個Map對象,它保存了該sp文件中存儲的所有鍵值對的信息。
  4. 最後把得到的Map對象賦值給mMap屬性,mStatTimestamp和mStatTimestamp用於判斷數據是否更新。

SharedPreferences數據在內存中的存儲結構

SharedPreferences文件都是存放在“/data/data/<app包名>/shared_prefs/”目錄下。sp文件的存儲格式是.xml文件,當SharedPreferences文件創建時,就會在相應目錄新建一個本地文件。

我們可以從ContextImpl中看到sp文件是如何管理的:

    private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

這裏會定義一個ArrayMap成員變量,存儲了當前應用的所有sp對象。這樣做是系統爲了性能考慮,在每個sp文件讀取之後,都會把sp對象存儲到一個map中作爲緩存。

ArrayMap對象的創建及賦值過程:

    @GuardedBy("ContextImpl.class")
    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;
    }

從代碼中我們可以看出,系統對SharedPreferences對象數據的存儲結構是什麼。

SharedPreferences對象數據的存儲結構:

  • 首先以包名爲key,存儲一個名爲sSharedPrefsCache的ArrayMap隊列中。
  • 在ArrayMap中,又是以文件名爲key,SharedPreferencesImpl對象(sp的實例對象)爲value存儲的。
  • 每一個SharedPreferencesImpl對象都對應一個物理位置在的sp文件。
  • SharedPreferencesImpl對象在創建時,或者sp文件有更新時,都會去存儲區同步數據文件,並且把數據文件存儲到一個map中(key/vaule分別對應我們存儲時的key/value),該map就是上文中提到的SharedPreferencesImpl對象的mMap屬性。

SharedPreferences的數據讀取過程分析

我們以獲取int值爲例,來看sp的數據讀取過程。

SharedPreferencesImpl類的getInt方法:

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

SharedPreferencesImpl類的getInt方法可以獲取一個在sp文件中存儲的int值。這裏可以看到,源碼中是直接從mMap中讀取的,而這個mMap是SharedPreferencesImpl在創建時初始化的。這種做法,可以避免每次讀取時,系統和存儲分區的交互,從而大幅度提升了性能。

其他幾種類型的數據讀取邏輯類似,這裏可以看到,讀取過程相對來說非常簡單,當SharedPreferencesImpl實例創建完成後,sp的xml文件中的數據已經加載到內存中,所以這裏獲取時,只需要簡單的內存查詢即可。

SharedPreferences數據存儲過程分析

數據存儲過程相對來說比較複雜,我們先來看如何使用sp來實現存儲。

SharedPreferences數據存儲示例

如果我們想要通過SharedPreferences存儲數據,代碼如下:

        SharedPreferences.Editor editor = getSharedPreferences("person", MODE_PRIVATE).edit();
        editor.putString("name","budaye");
        editor.putInt("age",18);
        editor.apply();

代碼執行之後,系統會在/data/data/<app包名>/shared_prefs/目錄下,創建一個名爲person.xml的文件:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="name">budaye</string>
    <int name="age" value="18" />
</map>

sp的數據存儲,需要藉助SharedPreferences的內部類Editor來實現,並且最後要使用apply()或commit()來保存更改。

我們來看源碼。

SharedPreferences.Editor的putInt方法分析

我們以putInt爲例,來分析sp數據的存儲過程。

SharedPreferences的edit()方法:
    public Editor edit() {
        synchronized (mLock) {
            awaitLoadedLocked();//等待鎖釋放
        }

        return new EditorImpl();
    }

該方法new一個EditorImpl對象並返回,所以SharedPreferences.Editor的實現類是EditorImpl。

EditorImpl的putInt方法
        @GuardedBy("mEditorLock")
        private final Map<String, Object> mModified = new HashMap<>();
        
        @Override
        public Editor putInt(String key, int value) {
            synchronized (mEditorLock) {
                mModified.put(key, value);
                return this;
            }
        }

這裏只是簡單的將key和value的值put到了mModified中,mModified是一個Map,它存儲者一次事務提交的所有將要變更的數據列表。

EditorImpl的apply方法

EditorImpl的apply方法:

        @Override
        public void apply() {
            final long startTime = System.currentTimeMillis();

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

            QueuedWork.addFinisher(awaitCommit);

            Runnable postWriteRunnable = new Runnable() {
                    @Override
                    public void run() {
                        awaitCommit.run();
                        QueuedWork.removeFinisher(awaitCommit);
                    }
                };

            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
            ……
            notifyListeners(mcr);
        }
邏輯解析:
  1. 調用commitToMemory()來把數據更新至內存中。
  2. apply()方法使用異步的方式來實現數據的寫入過程(寫入存儲分區)。
  3. 調用SharedPreferencesImpl的enqueueDiskWrite方法來執行數據寫入工作,注意這裏的第二個參數,如果不爲null,則代表異步寫入。

我們再來看commit()方法。

EditorImpl的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 {
                if (DEBUG) {
                    Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                            + " committed after " + (System.currentTimeMillis() - startTime)
                            + " ms");
                }
            }
            notifyListeners(mcr);
            return mcr.writeToDiskResult;
        }
邏輯解析:
  1. commit方法同樣先調用了commitToMemory()來把數據更新至內存中。
  2. 然後調用SharedPreferencesImpl的enqueueDiskWrite方法來執行數據寫入工作,注意這裏是同步寫入,第二個參數爲null。

apply和commit方法都調用了兩個關鍵方法:commitToMemory和SharedPreferencesImpl的enqueueDiskWrite方法,我們逐個分析。

EditorImpl的commitToMemory方法

        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); //如果其他線程也正在進行寫入操作,我們先把mMap的鍵值對複製出一份。
                }
                mapToWriteToDisk = mMap; //直接在mapToWriteToDisk上進行操作
                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) {//如果調用了Editor的clear,則先將map中的數據進行清除。
                        if (!mapToWriteToDisk.isEmpty()) {
                            changesMade = true;
                            mapToWriteToDisk.clear();
                        }
                        mClear = false;
                    }

                    for (Map.Entry<String, Object> e : mModified.entrySet()) { //遍歷mModified中的數據
                        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)) {//key和value都是空值,則跳過該條數據
                                continue;
                            }
                            mapToWriteToDisk.remove(k);//key值存在,value爲null,則將數據刪除。
                        } else {
                            if (mapToWriteToDisk.containsKey(k)) {
                                Object existingValue = mapToWriteToDisk.get(k);
                                if (existingValue != null && existingValue.equals(v)) {//key在map中已經存在,並且value沒有改變,則跳過
                                    continue;
                                }
                            }
                            mapToWriteToDisk.put(k, v);//將key/value寫入map中
                        }

                        changesMade = true;
                        if (hasListeners) {
                            keysModified.add(k);
                        }
                    }

                    mModified.clear();

                    if (changesMade) {
                        mCurrentMemoryStateGeneration++;
                    }

                    memoryStateGeneration = mCurrentMemoryStateGeneration;
                }
            }
            return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                    mapToWriteToDisk);//返回結果
        }
邏輯解析:
  1. mMap中存儲的是該sp文件的所有數據,是sp文件在內存中的映射。
  2. 判斷其他線程是否也正在進行寫入操作,如果是,則把mMap的鍵值對複製出一份。
  3. 將mMap賦值給mapToWriteToDisk變量,後面直接在mapToWriteToDisk上進行操作。
  4. 如果調用了Editor的clear,則將map中的數據進行清除。
  5. 遍歷mModified中的數據,mModified保存了本次事務提交的所有修改,上文中的putInt的數據,就存在該Map中。
  6. 判斷,key和value都是空值,則跳過該條數據。
  7. key值存在,value爲null,則將數據刪除。
  8. key在map中已經存在,並且value沒有改變,則跳過。
  9. 最後將key和value寫入mapToWriteToDisk中。
  10. 如果數據已經改變,則設置changesMade變量爲true。
  11. 最後返回已修改的內存數據對象MemoryCommitResult。

SharedPreferencesImpl的enqueueDiskWrite方法

數據在內存中更新之後,最後一步就是寫入存儲分區了,我們來看它對應的方法。

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

        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }
邏輯解析:

首先判斷是否需要同步,這裏可以看到apply是異步的,commit是同步的(有條件的同步)。
如果只有一個線程在執行寫入操作mDiskWritesInFlight的值是1,則直接調用writeToFile方法執行寫入工作。
否則,調用QueuedWork.queue方法添加到任務隊列中執行,等待執行。
這裏commit同步提交也是有條件的,如果commit時,該sp文件正在被其他線程執行數據寫入,則執行異步寫入。

Queued類的queue方法

我們來看異步寫入的執行:

    public static void queue(Runnable work, boolean shouldDelay) {
        Handler handler = getHandler();

        synchronized (sLock) {
            sWork.add(work);

            if (shouldDelay && sCanDelay) {
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);//如果是applay提交,這裏直接delay了100毫秒再執行
            } else {
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
    }

異步任務是通過HandlerThread來實現的,我們來看它的初始化過程:

    private static Handler getHandler() {
        synchronized (sLock) {
            if (sHandler == null) {
                HandlerThread handlerThread = new HandlerThread("queued-work-looper",
                        Process.THREAD_PRIORITY_FOREGROUND);
                handlerThread.start();

                sHandler = new QueuedWorkHandler(handlerThread.getLooper());
            }
            return sHandler;
        }
    }

這裏創建了一個HandlerThread來執行異步,也就是任務隊列是單線程的,並且線程優先級是前臺線程優先級。

SharedPreferencesImpl的writeToFile方法

寫入存儲分區真正的執行方法是SharedPreferencesImpl的writeToFile方法:

    private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
        ……
        boolean fileExists = mFile.exists(); //sp文件是否存在

        ……
        if (fileExists) {
            boolean needsWrite = false; //本次是否需要執行寫入操作

            // Only need to write if the disk state is older than this commit
            if (mDiskStateGeneration < mcr.memoryStateGeneration) { //mDiskStateGeneration是一個long類型的值,記錄着最後一次數據提交的狀態值。
                if (isFromSyncCommit) {//如果是同步寫入操作,這裏指commit提交的,直接執行寫入存儲分區操作
                    needsWrite = true;
                } else {
                    synchronized (mLock) {
                        // No need to persist intermediate states. Just wait for the latest state to
                        // be persisted.
                        if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) { //如果是異步寫入,這裏指apply提交的,則判斷是否是最新的一次寫入請求,如果不是,則不執行寫入存儲分區操作,以優化性能。
                            needsWrite = true;
                        }
                    }
                }
            }

            if (!needsWrite) { //不需要執行數據寫入
                mcr.setDiskWriteResult(false, true);
                return;
            }

            boolean backupFileExists = mBackupFile.exists();//判斷備份是否存在

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

            if (!backupFileExists) {//如果備份不存在,則把sp原始文件重命名爲備份文件。
                if (!mFile.renameTo(mBackupFile)) {
                    Log.e(TAG, "Couldn't rename file " + mFile
                          + " to backup file " + mBackupFile);
                    mcr.setDiskWriteResult(false, false);
                    return;
                }
            } else {//備份存在,則將file文件刪除,因爲它可能是錯誤數據。
                mFile.delete();
            }
        }
        //到了這裏,sp對應的原始文件已經被刪除了,只存在備份文件了。
        // 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);//創建一個空的原始文件,以存儲sp數據。

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

            if (str == null) {
                mcr.setDiskWriteResult(false, false);
                return;
            }
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);//執行xml數據解析,將內存中的key-value鍵值對存儲到str的數據流中。

            writeTime = System.currentTimeMillis();

            FileUtils.sync(str);//將數據流寫入到存儲分區中。

            fsyncTime = System.currentTimeMillis();

            str.close();
            ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);

            ……
            mBackupFile.delete();//寫入完成後,將備份文件刪除。

            ……

            mDiskStateGeneration = mcr.memoryStateGeneration;//更新mDiskStateGeneration的值,代表了最後一次寫入時的值

            mcr.setDiskWriteResult(true, true);

            ……

            return;
        } catch (XmlPullParserException e) {
            Log.w(TAG, "writeToFile: Got exception:", e);
        } catch (IOException e) {
            Log.w(TAG, "writeToFile: Got exception:", e);
        }

        // 如果磁盤寫入失敗,則刪除原始sp文件。
        if (mFile.exists()) {
            if (!mFile.delete()) {
                Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
            }
        }
        mcr.setDiskWriteResult(false, false);
    }
邏輯解析:

sp文件寫入存儲分區(磁盤)的工作是由SharedPreferencesImpl的writeToFile方法來完成的,邏輯較多,根據代碼邏輯,我們簡單總結一些它的過程:

  1. 首先使用fileExists變量來判斷sp文件是否存在。
  2. 如果存在,判斷本次是否需要執行寫入操作,用變量needsWrite表示。mDiskStateGeneration是一個long類型的值,記錄着最後一次數據提交的狀態值,用來判斷是否存在更新。
  3. 如果是commit提交的,則needsWrite賦值爲true,執行數據寫入。
  4. 如果是apply提交的,則判斷是否是最新的一次寫入請求如果是,則needsWrite賦值爲true,如果不是(是一箇中間提交值,之後還有提交),則不執行寫入存儲分區操作,以優化性能。
  5. 如果needsWrite的值爲false則不需要執行數據寫入,結束本次任務。
  6. 接下來判斷sp文件備份是否存在,如果備份不存在,則把sp原始文件重命名爲備份文件。如果備份存在,則將file文件刪除,因爲它可能是錯誤數據。
  7. 2~6是sp文件存在時的處理邏輯。到了這裏,sp對應的原始文件已經被刪除了,只存在備份文件了(如果存在的話)。
  8. 接下來執行寫入存儲分區,首先創建一個空文件,以存儲sp數據。
  9. 執行xml數據解析,將內存中的key-value鍵值對存儲到str的數據流中。
  10. 將數據流寫入到存儲分區中。
  11. 寫入完成後,將備份文件刪除。
  12. 更新mDiskStateGeneration的值,代表了最後一次寫入時的狀態值。
  13. 最後,如果磁盤寫入失敗,則刪除原始sp文件。

好了,到了這裏,SharedPreferences的實現原理我們也就分析完了,那麼在使用過程時,你是否也瞭解了SharedPreferences的正確打開方式呢?

SharedPreferences性能問題及最佳實踐


sp文件的io操作

  • sp文件存儲在“/data/data/<app包名>/shared_prefs/”目錄下,存儲格式是以.xml文件的形式存在。
  • sp文件在創建SharedPreferencesImpl對象時,會把文件從磁盤分區加載到內存中,並且存儲到Map中。
  • sp文件讀取是在子線程中進行的,子線程的優先級等同於它的父線程優先級。
  • sp文件在更新時,首先會更新到內存的Map對象中,根據提交的方式,commit通常會強制、同步寫入,apply會執行異步寫入工作,並且會等待100ms。
  • sp在異步提交時,使用的是ThreadHandler,線程優先級爲前臺任務的單線程操作,如果任務很多,怎會等待。
  • sp文件在寫入時,會刪除舊文件,新建新的文件,重新執行寫入。
  • sp文件的寫入更新方式是,全文件內容替換。
  • Context.MODE_MULTI_PROCESS模式時,每次調用getSharedPreferences獲取SharedPreferences對象時,都會檢查數據是否更新,如果更新,則從磁盤重新加載文件到內存中。

SharedPreferences的性能及最佳實踐

sp的性能問題:

  1. sp文件存儲在App私有目錄,所以會隨着App卸載而刪除。
  2. sp文件存儲的數據格式是.xml,每次從磁盤讀取和寫入操作,都需要解析xml,效率不高。
  3. sp的大量使用會佔用大量的內存,因爲它會把所有用到的sp文件內容都同步到內存中。
  4. sp錯誤使用,會導致大量的io操作,影響系統性能,例如,頻繁的commit或apply。
  5. Context.MODE_MULTI_PROCESS模式的誤用,會產生大量的io操作,嚴重影響性能。
  6. sp是在新建線程執行初始化工作,如果App啓動時,在主線程執行大量的sp初始化工作,會創建大量的線程,且線程優先級同UI線程,這樣會造成sp線程搶佔UI線程資源,造成啓動過慢等問題。
  7. sp如果是在優先級較低子線程中執行sp的初始化工作,則sp加載過程可能會變的很長。
  8. sp在提交時,如果在ui線程中使用commit同步提交,則可能會導致因等待而產生的ANR問題。
  9. sp每次更新到磁盤都是整體寫入,性能影響較大。
  10. sp在執行數據寫入時,都會創建EditorImpl對象,大量的提交操作會創建大量的EditorImpl對象,佔用大量內存。
  11. sp跨進程訪問模式,不可靠,已廢棄。
  12. 當單個sp文件大於50k時(經驗值,不同機器差別較大),io會變的非常緩慢。
  13. sp文件在執行apply寫入時,至少要等到100ms以上。

sp的最佳實踐

  1. 推薦使用sp存儲一些數據量較小的應用配置類信息。
  2. 不要使用sp的Context.MODE_MULTI_PROCESS模式;不要指望使用sp來進行跨進程數據操作。
  3. 單個sp文件大小最好保持在10kb以內,最大不要超過50kb。
  4. 將不同的業務數據保存在不同的sp文件內,不要一個文件存儲所有數據。
  5. sp數據更新時,最好多次修改後,統一執行一次commit或apply,以減少io次數。
  6. sp文件數量也要進行控制,以減少線程數量和內存使用。
  7. ui線程中使用sp數據要注意時效性,最好在使用之前,預加載到內存。
  8. sp加載時,會在子線程執行,子線程的優先級等同於父線程,一定要注意加載的時間。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章