Android SharedPreference 源碼分析

1.前言

衆所周知,SharedPreferences是Android平臺上一個輕量級的存儲類,用來保存應用的一些經常使用配置,好比Activity狀態,Activity暫停時,將此activity的狀態保存到SharedPereferences中;當Activity重載,系統回調方法onSaveInstanceState時,再從SharedPreferences中將值取出。android

2.基本概念

SharedPreferences提供了常規的Long、Int、String等類型數據的保存/獲取接口,並以xml方式保存在data/data/you.package.name/shared_prefs/目錄下的文件。 SharedPreferences數據的操做模式 Context.MODE_PRIVATE:爲默認操做模式,表明該文件是私有數據,只能被應用自己訪問,在該模式下,寫入的內容會覆蓋原文件的內容 Context.MODE_APPEND:模式會檢查文件是否存在,存在就往文件追加內容,不然就建立新文件. Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE用來控制其餘應用是否有權限讀寫該文件. MODE_WORLD_READABLE:表示當前文件能夠被其餘應用讀取. MODE_WORLD_WRITEABLE:表示當前文件能夠被其餘應用寫入 特別注意:出於安全性的考慮,MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE 在Android 4.2版本中已經被棄用。 MODE_MULTI_PROCESS:跨進程模式緩存

3.提出問題

1).SharedPreferences真的支持多進程嗎? 可能有些人會以爲很奇怪,上面不是寫了MODE_MULTI_PROCESS模式,明確說明就是跨進程模式,爲何還問是否真的支持多進程。爲了找到答案,咱們來閱讀MODE_MULTI_PROCESS的註釋。安全

/**
     * SharedPreference loading flag: when set, the file on disk will
     * be checked for modification even if the shared preferences
     * instance is already loaded in this process.  This behavior is
     * sometimes desired in cases where the application has multiple
     * processes, all writing to the same SharedPreferences file.
     * Generally there are better forms of communication between
     * processes, though.
     *
     * <p>This was the legacy (but undocumented) behavior in and
     * before Gingerbread (Android 2.3) and this flag is implied when
     * targetting such releases.  For applications targetting SDK
     * versions <em>greater than</em> Android 2.3, this flag must be
     * explicitly set if desired.
     *
     * @see #getSharedPreferences
     *
     * @deprecated MODE_MULTI_PROCESS does not work reliably in
     * some versions of Android, and furthermore does not provide any
     * mechanism for reconciling concurrent modifications across
     * processes.  Applications should not attempt to use it.  Instead,
     * they should use an explicit cross-process data management
     * approach such as {@link android.content.ContentProvider ContentProvider}.
     */
複製代碼

很尷尬,英文看不懂?google翻譯一下。bash

/ **
     * SharedPreference加載標誌:設置時,磁盤上的文件將
     *即便共享首選項,也要檢查修改
     *實例已經在這個過程當中加載。這是行爲
     *有時須要在應用程序有多個的狀況下
     *進程,所有寫入相同的SharedPreferences文件。
     *通常來講有更好的交流形式
     *進程,雖然。
     *
     *這是在和遺傳(但沒有記錄)的行爲
     *以前的薑餅(Android 2.3)和這個標誌是隱含的時候
     *瞄準這樣的發佈。針對SDK的應用程序
     *版本<em>大於</ em> Android 2.3,這個標誌必須是
     *若是須要顯式設置。
     *
     *參見#getSharedPreferences
     *
     * @deprecated MODE_MULTI_PROCESS不能可靠的工做
     *某些版本的Android,並且不提供任何
     *協調跨越併發修改的機制
     *進程。應用程序不該該嘗試使用它。代替,
     *他們應該使用明確的跨進程數據管理
     *方法,如{@link android.content.ContentProvider ContentProvider}。
     * /
複製代碼

很明顯MODE_MULTI_PROCESS是不可靠的,google推薦使用ContentProvider實現跨進程數據管理,如何使用,這個在下一篇介紹。 2).上面說到SharedPreference是以xml文件存在本地,那麼讀取/保存文件是否是損耗性能,需不須要每一個app本身作內存緩存,還有保存數據大小是否有限制,頻繁讀取有沒有注意點,帶着這些問題咱們看看SharedPreference底層是如何實現的。併發

4.源碼分析

1)先上圖,讓你們直觀的瞭解下SharedPreference相關的類以及相關關係。 app

這裏寫圖片描述

2)通常使用SharedPreference的時候,都是以下調用:異步

SharedPreferences sp = context.getSharedPreferences(SP_NAME,
                Context.MODE_MULTI_PROCESS);
複製代碼

那麼,咱們就看下getSharedPreferences(String name, int mode)方法,打開Context類能夠看到該方法是個抽象方法,具體實如今子類中,打開Context的實現類ContextImpl,能夠看到getSharedPreferences(String name, int mode)源碼以下所示:ide

@Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            if (sSharedPrefs == null) {
                sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
            }

            final String packageName = getPackageName();
            ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
            if (packagePrefs == null) {
                packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();
                sSharedPrefs.put(packageName, packagePrefs);
            }

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

            sp = packagePrefs.get(name);
            if (sp == null) {
                File prefsFile = getSharedPrefsFile(name);
                sp = new SharedPreferencesImpl(prefsFile, mode);
                packagePrefs.put(name, 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;
    }
複製代碼

從5-14行能夠看出,先根據應用包名,從本地map中取到該應用的全部sp文件,在根據傳進來的name,獲取須要讀取的sp文件。 從27-32行能夠看出,第一次先拿到sp文件,而後初始化出SharedPreferencesImpl實現類,在SharedPreferencesImpl的構造方法中從本地讀取了該name的sp文件,後面會分析。 從34-41行看出,若是是MODE_MULTI_PROCESS模式或者版本小於11,會調用sp.startReloadIfChangedUnexpectedly()方法,該方法從磁盤把文件從新讀取到內存,源碼以下:源碼分析

void startReloadIfChangedUnexpectedly() {
        synchronized (this) {
            // TODO: wait for any pending writes to disk?
            if (!hasFileChangedUnexpectedly()) {
                return;
            }
            startLoadFromDisk();
        }
    }
複製代碼

剛剛說到SharedPreferencesImpl的構造方法中從本地讀取了該name的sp文件,源碼以下所示:post

SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        startLoadFromDisk();
    }
複製代碼
private void startLoadFromDisk() {
        synchronized (this) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                synchronized (SharedPreferencesImpl.this) {
                    loadFromDiskLocked();
                }
            }
        }.start();
    }
複製代碼
private void loadFromDiskLocked() {
        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 {
            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 (XmlPullParserException e) {
                    Log.w(TAG, "getSharedPreferences", e);
                } catch (FileNotFoundException e) {
                    Log.w(TAG, "getSharedPreferences", e);
                } catch (IOException e) {
                    Log.w(TAG, "getSharedPreferences", e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
        }
        mLoaded = true;
        if (map != null) {
            mMap = map;
            mStatTimestamp = stat.st_mtime;
            mStatSize = stat.st_size;
        } else {
            mMap = new HashMap<String, Object>();
        }
        notifyAll();
    }
複製代碼

從上面代碼能夠看出,從sp文件讀出數據後,會賦值給mMap對象,該對象經過鍵值對保存sp文件,實現二級緩存的目的是爲了提升讀取效率,因此app應用中就不須要單獨再本身作緩存。既然數據都保存在內存緩存mMap中,這樣也就說明sp文件不適合存儲大數據,會十分浪費內存。

下面再看下,如何保存數據,咱們通常使用兩種方法:apply(),commit()

public void apply() {
            final MemoryCommitResult mcr = commitToMemory();
            final Runnable awaitCommit = new Runnable() {
                    public void run() {
                        try {
                            mcr.writtenToDiskLatch.await();
                        } catch (InterruptedException ignored) {
                        }
                    }
                };

            QueuedWork.add(awaitCommit);

            Runnable postWriteRunnable = new Runnable() {
                    public void run() {
                        awaitCommit.run();
                        QueuedWork.remove(awaitCommit);
                    }
                };

            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

            // 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); } 複製代碼

第二行能夠看出,先保存到內存,經過第十二行,能夠看出,經過異步線程保存sp文件到磁盤。

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;
            }
            notifyListeners(mcr);
            return mcr.writeToDiskResult;
        }
複製代碼

經過commit的源碼能夠看出,該保存到內存跟磁盤是同步保存,因此,若是頻繁保存數據的話,apply確定要高效,優先推薦使用apply。

看完保存數據,下面咱們來看如何讀取數據,拿getString來作示例:

@Nullable
    public String getString(String key, @Nullable String defValue) {
        synchronized (this) {
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }
複製代碼
private void awaitLoadedLocked() {
        if (!mLoaded) {
            // Raise an explicit StrictMode onReadFromDisk for this
            // thread, since the real read will be in a different
            // thread and otherwise ignored by StrictMode.
            BlockGuard.getThreadPolicy().onReadFromDisk();
        }
        while (!mLoaded) {
            try {
                wait();
            } catch (InterruptedException unused) {
            }
        }
    }
複製代碼

能夠看到數據是從mMap中讀取,也就是從內存緩存中取,這樣省去io操做,提升性能。 裏面還有一個boolean變量mLoaded,該變量默認爲false,這樣會一直阻塞住讀取,在什麼地方設置爲true的呢,從mLoaded的字面能夠猜到是加載,那應該是從本地磁盤讀成功賦值爲true,咱們看下源碼:

private void loadFromDiskLocked() {
        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 {
            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 (XmlPullParserException e) {
                    Log.w(TAG, "getSharedPreferences", e);
                } catch (FileNotFoundException e) {
                    Log.w(TAG, "getSharedPreferences", e);
                } catch (IOException e) {
                    Log.w(TAG, "getSharedPreferences", e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
        }
        mLoaded = true;
        if (map != null) {
            mMap = map;
            mStatTimestamp = stat.st_mtime;
            mStatSize = stat.st_size;
        } else {
            mMap = new HashMap<String, Object>();
        }
        notifyAll();
    }
複製代碼

很明顯,最後讀取成功後,mLoaded 被賦值爲true,而loadFromDiskLocked這個方法是在SharedPreferencesImpl的構造方法中就調用,因此爲了避免形成阻塞,咱們能夠提早建立出SharedPreferences對象,而不是在使用的時候再去建立。

總結: 1.sp數據都保存在內存緩存mMap中,這樣也就說明sp文件不適合存儲大數據,會十分浪費內存。 2.apply()先保存到內存,再經過異步線程保存sp文件到磁盤,commit保存到內存跟磁盤是同步保存,因此,若是頻繁保存數據的話,apply確定要高效,優先推薦使用apply。 3.從sp中讀取數據,須要等sp構造方法調用從磁盤讀取數據成功後才繼續往下執行,因此爲了避免形成阻塞,咱們能夠提早建立出SharedPreferences對象,而不是在使用的時候再去建立。


若有錯誤歡迎指出來,一塊兒學習。

在這裏插入圖片描述
相關文章
相關標籤/搜索