Android開發-數據存儲SharedPreferences工具類、Set保存問題、源碼分析

介紹

SharedPreferences作爲Android提供給我們方便簡單的存儲數據的類。它內部的實現實際上是xml格式的文件存儲數據,同時爲了提升讀寫性能同時實現了內存緩存機制。關鍵源碼在android.app包中的SharedPreferencesImpl類裏面。值得一提的是Context實例的getSharedPreferences是抽象方法,看不到實現。因爲整個Context套件被設計成裝飾者模式
推薦一篇分析源碼的博客:
Android SharedPreferences 源碼分析,寫得很好很詳細。

讀寫工具類

SharedPreferences數據的存儲作爲一個常用的功能模塊,我們都會專門寫一個讀寫工具類方便使用。
一般有兩種包裝工具類方式:

確定每個讀寫方法

提前確定好每個確定的key值和保存類型,直接提供某個字段的讀寫方法,示例如下:

public static void setUserName(Context context,String value){
        //做具體的寫入操作 使用apply()沒有返回值
    }

這樣寫的好處是每個方法的操作明確易於理解。缺點是不易拓展每個字段都需要一個專門的方法去操作,如果存儲的字段很多相應的方法也會變得很多。如果再抽象的寫,一種存儲類型一個方法

public static void putString(Context context,String key,String value){
        //針對String類型 統一使用該方法
    }

這裏有個問題,如果這個存儲類型的方法還需要區分apply()和commit()兩種寫入方式,代碼量還是很多。

統一處理

統一傳入參數類型爲Object,然後用instanceof做類型判斷簡化代碼。這也是我現在使用的方式,提供關鍵代碼給大家參考。

/**
     * 異步提交方法
     * @param context
     * @param key
     * @param object
     */
    public static void putApply(Context context, String key, Object object) {
        SharedPreferences sp = context.getSharedPreferences(FILE_NAME,
                MODE);
        SharedPreferences.Editor editor = sp.edit();
        judgePutDataType(key, object, editor);
        editor.apply();
    }

    /**
     * 同步提交方法
     * @param context
     * @param key
     * @param object
     * @return
     */
    public static boolean putCommit(Context context, String key, Object object){
        SharedPreferences sp = context.getSharedPreferences(FILE_NAME,
                MODE);
        SharedPreferences.Editor editor = sp.edit();
        judgePutDataType(key, object, editor);
        return editor.commit();
    }

    /**
     * 根據不同類型 使用不同的寫入方法
     * @param key
     * @param object
     * @param editor
     */
    private static void judgePutDataType(String key, Object object, SharedPreferences.Editor editor) {
        if (object instanceof String) {
            editor.putString(key, (String) object);
        } else if (object instanceof Integer) {
            editor.putInt(key, (Integer) object);
        } else if (object instanceof Boolean) {
            editor.putBoolean(key, (Boolean) object);
        } else if (object instanceof Float) {
            editor.putFloat(key, (Float) object);
        } else if (object instanceof Long) {
            editor.putLong(key, (Long) object);
        } else if (object instanceof Set) {
            editor.putStringSet(key, (Set<String>) object);
        } else {
            editor.putString(key, object.toString());
        }
    }

以上的代碼已經可以覆蓋所有的寫入類型,然後再做不同的寫入方法處理。簡化了代碼量。缺點是每次都是包裝類型傳入使用時需要注意,同時包裝類也會佔用更多內存空間。優點就是代碼少。大家根據實際情況考慮使用。

存儲Set的問題

當我們需要存儲不關心存儲順序的String對象時候可以考慮使用putStringSet(String key, @Nullable Set<String> values)用Set<>集合存數據。
Set集合有一個特性就是,不會存入已經存在的元素,利用這個特性可以簡化不少寫入檢查代碼。但是使用不當會出現意想不到的問題。
當時我的代碼如下:

private void saveSearchHistory(String key) {
        //保存搜索記錄
        HashSet<String> hashSet = (HashSet<String>) SPUtils.get(mContext, Constant.HISTORYTEXT, new HashSet<String>());
       //直接在返回對象上修改了值
        hashSet.add(key);
        boolean isSuccess = SPUtils.putCommit(mContext, Constant.HISTORYTEXT, hashSet);
        //打印同步寫入的結果
        Logger.d("isSuccess=" + isSuccess);
    }

這個代碼片每次運行都能打印出寫入成功的返回值。當存儲的數據數量超過1個的時候就有問題。如果整個應用退出再次進入,讀取保存的數據就有可能只會讀到1個值。多次檢查代碼都沒有問題。最後在stackoverflow上看到和我一樣的情況。幡然醒悟!
回到Android開發文檔有這麼一段話:

Note that you must not modify the set instance returned by this call. The consistency of the stored data is not guaranteed if you do, nor is your ability to modify the instance at all.
翻譯:請注意,您不能修改此調用返回的集合實例。如果你做了,存儲的數據的一致性是不保證的,也不是你的能力來修改的實例。

文檔提示我們不能直接修改返回的實例,所以我們代碼需要這麼寫。

private void saveSearchHistory(String key) {
        //轉到這個界面就表示 搜索成功 保存搜索記錄
        HashSet<String> hashSet = (HashSet<String>) SPUtils.get(mContext, Constant.HISTORYTEXT, new HashSet<String>());
        //關鍵操作 需要在新的集合添加值 然後再提交修改
        Set<String> changeData = new HashSet<>(hashSet);
        changeData.add(key);

        boolean isSuccess = SPUtils.putCommit(mContext, Constant.HISTORYTEXT, changeData);
        Logger.d("isSuccess=" + isSuccess);
    }

問題解決了。
另外一種解決方案是:先取出集合數據,再刪掉這個key值保存的數據,修改取出的集合數據,再次提交寫入。需要做兩次修改。感覺這個方案比較亂就沒有實踐。
具體爲什麼會這樣去看源碼或許能找到你自己的答案。推薦Android SharedPreferences 源碼分析幫助理解。

保證存儲順序的數組

當我們需要存儲的數據需要確定每次的存儲順序,目前的網絡上的解決方案是拼接String字符串用逗號“,”分割,和逗號一起順序寫入,取出時用逗號“,”,做分割符用String對象的public String[] split(String regularExpression) {}方法或得數組。這樣順序就可以得到保證。具體實現很簡單而且方法可以多種我就不說明了。

關鍵源碼

既然使用到SharedPreferences作爲讀取數據的實現組件,就有必要瞭解到一些它的內部實現原理。
Android源碼的app包的SharedPreferencesImpl是SharedPreferences組件的真正實現類。
文件結構

成員變量

final class SharedPreferencesImpl implements SharedPreferences {
    private final File mFile;//文件存儲數據
    private final File mBackupFile;//備份文件
    private final int mMode;//讀寫模式

    private Map<String, Object> mMap;     // guarded by 'this' 內部緩存
    private int mDiskWritesInFlight = 0;  // guarded by 'this'
    private boolean mLoaded = false;      // guarded by 'this'
    private long mStatTimestamp;          // guarded by 'this'
    private long mStatSize;               // guarded by 'this'

    private final Object mWritingToDiskLock = new Object();//寫鎖對象 
    private static final Object mContent = new Object();
    private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners =
            new WeakHashMap<OnSharedPreferenceChangeListener, Object>();
            //弱引用持有的 數據變化監聽器 防止內存泄露的寫法  
            //省略很多代碼
            }

從這幾個成員變量就可以知道大概的實現原理

  • SharedPreferences以文件形式存儲數據。File mFile就是讀寫的文件對象。
  • 爲了提升讀寫效率,Map

構造方法

從構成方法的邏輯一步一步的看源碼。

SharedPreferencesImpl構造方法–>startLoadFromDisk開啓子線程讀取文件–>loadFromDiskLocked讀到文件流格式轉換–>賦值給緩存Map

讀文件肯定是在子線程執行:

//startLoadFromDisk 部分源碼
 new Thread("SharedPreferencesImpl-load") {
            public void run() {
                synchronized (SharedPreferencesImpl.this) {
                    loadFromDiskLocked();
                }
            }
        }.start();

XML格式文件使用XmlUtils工具加載解析

取數據

取數據相對簡單 ,比如getString就很簡單。

@Nullable
    public String getString(String key, @Nullable String defValue) {
        synchronized (this) {
            awaitLoadedLocked();//等待文件讀取完成 才能下一步
            String v = (String)mMap.get(key);//先從緩存取數據
            return v != null ? v : defValue;//根據取出的緩存數據 做判斷返回值
        }
    }

寫數據

我們都知道使用SharedPreferences寫數據是需要Editor對象,而它的實現類是EditorImpl

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) {
            synchronized (this) {
                mModified.put(key, value);
                return this;
            }
        }

apply

異步寫操作

 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);//向寫隊列 存入Runnable操作

            // 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);//發出通知 
        }

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;
            }
            //等待寫入之後 發出通知 
            notifyListeners(mcr);
            return mcr.writeToDiskResult;//並返回寫操作結果
        }

其他

SharedPreferences內部有一個寫隊列,當發出指令,會在線程池執行寫操作。單線程執行。

QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);

水平有限,只能寫這麼多了。借用Android SharedPreferences 源碼分析的話

writeToDiskRunnable中調用writeToFile寫文件。如果參數中的postWriteRunable爲null,則該Runnable會被同步執行,而如果不爲null,則會將該Runnable放入線程池中異步執行。在這裏也驗證了之前提到的commit和apply的區別。

總結

  1. 本文主要總結有關使用SharedPreferences組件的記錄
  2. 提供包裝工具類的兩種實現方法,並分析優缺點。
  3. 總結使用過程中,發現當存儲Set集合的遇到的存儲數據出錯問題,並提供解決方案。
  4. 記錄保證存儲順序的數組實現思路。
  5. 分析了部分源碼,瞭解內部構造和實現,當我們使用SharedPreferences存儲數據時要結合實際思考。

思考

我的項目中使用SharedPreferences存儲用戶信息,目前是直接的讀寫數據。爲了統一數據操作。

思考:是否有必要再寫一個用戶對象單例,緩存用戶數據?

問題是在我看《App研發錄》-User是唯一例外的全局變量章節的思考。

參考

  1. Android-API
  2. Android SharedPreferences 源碼分析
  3. 《App研發錄》
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章