史上最全面,清晰的SharedPreferences解析

基礎用法

獲取Sp:

  1. Activity中:getPreferences(int mode)
  2. context.getSharedPreferences(String name, int mode)
  3. PreferenceManager.getDefaultSharedPreferences(Context context)

1和3的獲取SP的方法最終都會調用2,只是1和3默認選取了特定的name,1中通過getLocalClassName()獲取通過包名和類名拼裝的name,3通過context.getPackageName() + "_preferences"獲取name

注意第二個參數的含義,現在均指定爲MODE_PRIVATE,其餘的都被廢棄。含義如下:File creation mode: the default mode, where the created file can only be accessed by the calling application (or all applications sharing the same user ID).
所存儲的數據保存在:/data/data/<package name>/shared_prefs下的指定name.xml文件中

get

sp.getX(String key, X value);
  • X爲簡單的基本類型:float,int,long,String,Boolean,Set

put

        SharedPreferences sharedPreferences = getPreferences(0);
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putFloat("float", 1f);
        editor.putBoolean("boolean", true);
        editor.apply();
  • 首先獲取Editor對象,操作完需要進行事務提交操作,可以採用commit或者apply進行。commit同步寫磁盤,返回是否成功的標識碼。apply異步寫磁盤,無返回值。(二者均是同步寫內存,先同步寫內存,之後同步/異步寫磁盤)

監聽器

        sharedPreferences.registerOnSharedPreferenceChangeListener(new SharedPreferences.OnSharedPreferenceChangeListener() {
            @Override
            public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {

            }
        });
  • 如果使用匿名內部類的形式進行監聽。注意,因爲OnSharedPreferenceChangeListener的引用被保存在一個WeakHashMap中,導致程序的行爲不確定性。爲了避免這種情況,推薦以下方式:
private OnSharedPreferenceChangeListener mListener = new OnSharedPreferenceChangeListener() {

  @Override
  public void onSharedPreferenceChanged(
      SharedPreferences sharedPreferences, String key) {
      Log.i(LOGTAG, "instance variable key=" + key);
  }
};

@Override
protected void onResume() {
  PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).registerOnSharedPreferenceChangeListener(mListener);
  super.onResume();
}

@Override
protected void onPause() {
  PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).unregisterOnSharedPreferenceChangeListener(mListener);
  super.onPause();
}

原理分析

獲取SharedPreferences

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

可見 sdk 是先取了緩存(sSharedPrefs靜態變量), 如果緩存未命中, 才構造對象. 也就是說, 多次 getSharedPreferences 幾乎是沒有代價的. 同時, 實例的構造被 synchronized 關鍵字包裹, 因此構造過程是多線程安全的

構造SharedPreferences

  • 第一次構建SharedPreferences對象
// SharedPreferencesImpl.java
SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    startLoadFromDisk();
}

幾個關鍵類成員信息解釋如下
1. mFile 代表我們磁盤上的配置文件
2. mBackupFile 是一個災備文件, 用戶寫入失敗時進行恢復, 後面會再說. 其路徑是 mFile 加後綴 ‘.bak’
3. mMap 用於在內存中緩存我們的配置數據, 也就是 getXxx 數據的來源

重點關注startLoadFromDisk()方法

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

開啓了一個從Disk讀取的線程

// SharedPreferencesImpl.java
private void loadFromDisk() {
    synchronized (SharedPreferencesImpl.this) {
        if (mLoaded) {
            return;
        }
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }

    ... 略去無關代碼 ...

    str = new BufferedInputStream(
            new FileInputStream(mFile), 16*1024);
    map = XmlUtils.readMapXml(str);

    synchronized (SharedPreferencesImpl.this) {
        mLoaded = true;
        if (map != null) {
            mMap = map;
            mStatTimestamp = stat.st_mtime;
            mStatSize = stat.st_size;
        } else {
            mMap = new HashMap<>();;
        }
        notifyAll();
    }
}

loadFromDisk()非常關鍵,他總共做了以下幾件事
1. 如果有 ‘災備’ 文件, 則直接使用災備文件回滾.
2. 把配置從磁盤讀取到內存的並保存在 mMap 字段中(看代碼最後 mMap = map)
3. 標記讀取完成, 這個字段後面 awaitLoadedLocked 會用到. 記錄讀取文件的時間, 後面 MODE_MULTI_PROCESS 中會用到
4. 發一個 notifyAll 通知已經讀取完畢, 激活所有等待加載的其他線程

這裏寫圖片描述

getX原理分析

    public float getFloat(String key, float defValue) {
        synchronized (this) {
            awaitLoadedLocked();
            Float v = (Float)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

關鍵信息如下:
1. synchronized保證了線程安全
2. get操作一定是從mMap中讀取,既從內存中讀取,無過多性能損耗。
3. awaitLoadedLocked()保證了讀取操作一定在loadFromDisk()執行之完,同步等待。因此第一次調用get操作可能會阻塞,萬分注意,這也是sp被定義爲輕量級存儲系統的重要原因

putX原理分析

put操作較爲複雜,一步一步分析

創建editor

// SharedPreferencesImpl.java
    public Editor edit() {
        // TODO: remove the need to call awaitLoadedLocked() when
        // requesting an editor.  will require some work on the
        // Editor, but then we should be able to do:
        //
        //      context.getSharedPreferences(..).edit().putString(..).apply()
        //
        // ... all without blocking.
        synchronized (this) {
            awaitLoadedLocked();
        }

        return new EditorImpl();
    }

EditorImpl()無構造函數,僅僅去初始化兩個成員變量

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

    ... 略去方法定義 ...
    public Editor putString(String key, @Nullable String value) { ... }
    public boolean commit() { ... }
    ...
}

關鍵信息如下:
1. ·mModified 是我們每次 putXxx 後所改變的配置項
2. mClear 標識要清空配置項

putString

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

很簡單, 僅僅是把我們設置的配置項放到了 mModified 屬性裏保存. 等到 apply 或者 commit 的時候回寫到內存和磁盤. 咱們分別來看看

apply

// SharedPreferencesImpl.java
public void apply() {
    final MemoryCommitResult mcr = commitToMemory();

    ... 略無關 ...

    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
}

apply核心在於兩點
1. commitToMemory()完成了內存的同步回寫
2. enqueueDiskWrite() 完成了硬盤的異步回寫, 我們接下來具體看看

// SharedPreferencesImpl.java
private MemoryCommitResult commitToMemory() {
    MemoryCommitResult mcr = new MemoryCommitResult();
    synchronized (SharedPreferencesImpl.this) {

        ... 略去無關 ...

        mcr.mapToWriteToDisk = mMap;
        mDiskWritesInFlight++;

        synchronized (this) {
            for (Map.Entry&lt;String, Object&gt; e : mModified.entrySet()) {
                String k = e.getKey();
                Object v = e.getValue();
                // &quot;this&quot; is the magic value for a removal mutation. In addition,
                // setting a value to &quot;null&quot; for a given key is specified to be
                // equivalent to calling remove on that key.
                if (v == this || v == null) {
                    mMap.remove(k);
                } else {
                    mMap.put(k, v);
                }
            }

            mModified.clear();
        }
    }
    return mcr;
}

兩個關鍵信息
1. 把 Editor.mModified 中的配置項回寫到 SharedPreferences.mMap 中, 完成了內存的同步
2. 把 SharedPreferences.mMap 保存在了 mcr.mapToWriteToDisk 中. 而後者就是即將要回寫到磁盤的數據源

// SharedPreferencesImpl.java
private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                final Runnable postWriteRunnable) {
    final Runnable writeToDiskRunnable = new Runnable() {
            public void run() {
                synchronized (mWritingToDiskLock) {
                    writeToFile(mcr);
                }

                ...
            }
        };

    ...

    QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}

關鍵信息
使用singleThreadExecutor單一線程池去依次執行寫入磁盤的runnable序列

  • 之後是真正執行把數據寫入磁盤的方法
// SharedPreferencesImpl.java
private void writeToFile(MemoryCommitResult mcr) {
    // Rename the current file so it may be used as a backup during the next read
    if (mFile.exists()) {
        if (!mBackupFile.exists()) {
            if (!mFile.renameTo(mBackupFile)) {
                return;
            }
        } else {
            mFile.delete();
        }
    }

    // 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);
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
        ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
        try {
            final StructStat stat = Os.stat(mFile.getPath());
                mStatTimestamp = stat.st_mtime;
                mStatSize = stat.st_size;
        }
        // Writing was successful, delete the backup file if there is one.
        mBackupFile.delete();
        return;
    }

    // Clean up an unsuccessfully written file
    mFile.delete();
}

主要分爲三個過程:
1. 先把已存在的老的配置文件重命名(加 ‘.bak’ 後綴), 然後刪除老的配置文件. 這相當於做了災備
2. 向 mFile 中一次性寫入所有配置項. 即 mcr.mapToWriteToDisk(這就是 commitToMemory 所說的保存了所有配置項的字段) 一次性寫入到磁盤. 如果寫入成功則刪除災備文件, 同時記錄了這次同步的時間
3. 如果上述過程 [2] 失敗, 則刪除這個半成品的配置文件

apply總結

由於apply比較複雜,稍作總結:
1. 通過 commitToMemory 將修改的配置項同步回寫到內存 SharedPreferences.mMap 中. 此時, 任何的 getXxx 都可以獲取到最新數據了
2. 通過 enqueueDiskWrite 調用 writeToFile 將所有配置項一次性異步回寫到磁盤. 這是一個單線程的線程池

這裏寫圖片描述

commit

  • commit比較簡單,直接看代碼和時序圖即可,大致和apply相同
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最後會等待異步任務返回,說明會阻塞當前調用線程,因此說commit是同步寫入,apply是異步寫入。

以上涵蓋了大部分SharedPreferences重要源碼分析,下面總結SharedPreferences最佳實踐,提出日後要注意的問題,只說結論不解釋原因。(如果你不明白爲什麼,證明你前面的分析沒有深刻理解)

SharedPreferences最佳實踐

勿存儲過大value

  • 永遠記住,SharedPreferences是一個輕量級的存儲系統,不要存過多且複雜的數據,這會帶來以下的問題
    • 第一次從sp中獲取值的時候,有可能阻塞主線程,使界面卡頓、掉幀。
    • 這些key和value會永遠存在於內存之中,佔用大量內存。

勿存儲複雜數據

  • SharedPreferences通過xml存儲解析,JSON或者HTML格式存放在sp裏面的時候,需要轉義,這樣會帶來很多&這種特殊符號,sp在解析碰到這個特殊符號的時候會進行特殊的處理,引發額外的字符串拼接以及函數調用開銷。如果數據量大且複雜,嚴重時可能導頻繁GC。

不要亂edit和apply,儘量批量修改一次提交

  • edit會創建editor對象,每進行一次apply就會創建線程,進行內存和磁盤的同步,千萬寫類似下面的代碼
SharedPreferences sp = getSharedPreferences("test", MODE_PRIVATE);
sp.edit().putString("test1", "sss").apply();
sp.edit().putString("test2", "sss").apply();
sp.edit().putString("test3", "sss").apply();
sp.edit().putString("test4", "sss").apply();

建議apply,少用commit

  • commit同步寫內存,同步寫磁盤。有是否成功的返回值
  • apply同步寫內存,異步寫磁盤。無返回值

registerOnSharedPreferenceChangeListener弱引用問題

  • 見本文初

apply和commit對registerOnSharedPreferenceChangeListener的影響

  • 對於 apply, listener 回調時內存已經完成同步, 但是異步磁盤任務不保證是否完成
  • 對於 commit, listener 回調時內存和磁盤都已經同步完畢

不要有任何用SP進行多進程存儲的幻想

  • 這個話題不需要過多討論,只記住一點,多進程別用SP,Android沒有對SP在多進程上的表現做任何約束和保證。附上Google官方註釋
@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}.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章