使用SharedPreferences进行数据存储
SharedPreferences是Android之中的基礎內容,是一種非常輕量化的存儲工具。核心思想就是在xml文件中保存鍵值對。而正因為采用的是文件讀寫,所以它天生線程不安全。Google曾經(jīng)想要對其進行一番擴展以令其實現(xiàn)線程安全讀寫,但最終以失敗告終。后來于是有了民間替代方案,詳細可以參考GitHub上這個項目。
筆者本身對SharedPreferences是否線程安全是沒有需求的,我主要是覺得它——
限、制、太、多!使、用、太、麻、煩!
吐槽及預期
// get it SharedPreferences p = mContext.getSharedPreferences("Myprefs", Context.MODE_PRIVATE); // or p = PreferenceManager.getDefaultSharedPreferences(mContext);// read p.getString("preference_key", "default value");// write p.edit().putString("preference_key", "new value").commit(); // or p.edit().putString("preference_key", "new value").apply();這里演示了String類型的情況,其他也是類似。
以上就是SharedPreferences的基本使用情況了,足以應付絕大部分情況,看上去也就那么幾行,挺簡單、挺好用的嘛!
那好,我們現(xiàn)在來看一下它究竟有哪些短板。
限制之一,使用之前必須拿到Context:
// get it SharedPreferences p = mContext.getSharedPreferences("Myprefs", Context.MODE_PRIVATE); // or p = PreferenceManager.getDefaultSharedPreferences(mContext);這里展示了兩種方式,第一種的優(yōu)勢是可以自定義名稱,并且如果需要的話可以指定全局讀寫(雖然Google不推薦用SharedPreferences來跨應用讀寫,相關字段早就被置上了deprecated),如果不需要則純粹成了消耗多余體力的代碼。
而且,Context并不是永遠都那么好拿的,所以有一種最簡單粗暴的作法就是做一個自己的Application類像是這樣:
但是殺雞焉用牛刀,你做這樣一個全局可得的ApplicationContext本就是為了不時之需,拿來用SharedPreferences,每次還得這樣寫App.getInstance(),逼格太低又很累啊。
限制之二,讀值為什么會要這么多代碼:
// read p.getString("preference_key", "default value");初看上去,這似乎是無比正常的代碼:"default value"的存在確保了你永遠可以取到值,但問題就出在這個"default value"上了,在某種情況下,你需要取某個值的地方很多,而且全都可能還沒有初始化過,也就是說在這些地方實際第一次處理時使用到值的是"default value",假如某一天"default value"值需要變更,你就要細心謹慎地把每個地方都改一輪了。
限制之三,寫值代碼也很多:
// write p.edit().putString("preference_key", "new value").commit(); // or p.edit().putString("preference_key", "new value").apply();先拿到Editor內部類,再操作,最后再提交,雖然IDE自帶補全功能,但補全三次也不是那么方便吧?源碼中的說法是,“so you can chain put calls together.”,因為每次putXXX()操作后仍舊返回同一個Editor內部類對象,所以你能一次性put許多下最后再提交。可實際情況中使用到鏈式調用的機會還是挺少的,畢竟很難出現(xiàn)Web上那種出現(xiàn)一整個表單給用戶填寫,最后一次性提交的情況。
總的來說,在不同的地方重復獲取SharedPreferences是沒有必要的,可以拿一個單例來解決;讀值和寫值太累贅了,要做下封裝……
不,這還不夠,作為一個名有追求的工程師——
我們需要一個強有力的Library來解決這些問題,力爭達到一經(jīng)寫就,永久受益的效果。
常規(guī)解決方案
一般是做一個單例工具類,然后簡單封裝一下方法,這里截取了一下Notes中的部分代碼如下:
/*** Created by lgp on 2014/10/30.*/ public class PreferenceUtils{private SharedPreferences sharedPreferences;private SharedPreferences.Editor shareEditor;private static PreferenceUtils preferenceUtils = null;public static final String NOTE_TYPE_KEY = "NOTE_TYPE_KEY";public static final String EVERNOTE_ACCOUNT_KEY = "EVERNOTE_ACCOUNT_KEY";public static final String EVERNOTE_NOTEBOOK_GUID_KEY = "EVERNOTE_NOTEBOOK_GUID_KEY";@Inject @Singletonprotected PreferenceUtils(@ContextLifeCycle("App") Context context){sharedPreferences = context.getSharedPreferences(SettingFragment.PREFERENCE_FILE_NAME, Context.MODE_PRIVATE);shareEditor = sharedPreferences.edit();}public static PreferenceUtils getInstance(Context context){if (preferenceUtils == null) {synchronized (PreferenceUtils.class) {if (preferenceUtils == null) {preferenceUtils = new PreferenceUtils(context.getApplicationContext());}}}return preferenceUtils;}public String getStringParam(String key){return getStringParam(key, "");}public String getStringParam(String key, String defaultString){return sharedPreferences.getString(key, defaultString);}public void saveParam(String key, String value){shareEditor.putString(key,value).commit();}...... }可以看到其思想還是挺簡單的,基本上對于限制一二三全都照顧到了。
對于限制一,因為是單例,只要明確這個類已經(jīng)初始化過一次了,后面就可以這樣來獲取實例PreferenceUtils.getInstance(null)——必須說明這是一種取巧的手段,而且看上去非常丑陋——所以說不需要依賴Context(另外我們還可以增加對于resId的支持,讓這種方式成為可能getStringParam(int resId)只要在這個類中持有Context就能做到——但要注意為防內存泄漏應給這個類傳ApplicationContext);關鍵是限制二的解決并不漂亮,因為不同的設置項的default值多數(shù)情況下是不一樣的,所以還是提供了一個二參方法getStringParam(String key, String defaultString),本質上并沒有解決。
不過不管怎樣,我們的Library LitePreferences最起碼要包含以上這個工具類的全部功能,然后再談突破。
極致簡約
既然是個單例,那么在使用之前就必須調用getInstance()了,像是這樣:
LitePrefs.getInstance(mContext).getInt(R.string.tedious);在這行代碼中,如果LitePrefs已經(jīng)初始化過一次了,那么中間的getInstance(mContext)純粹就是毫無意義。我們希望代碼簡約成這樣:
LitePrefs.getInt(R.string.tedious);要達到這樣的效果,只需讓getInt()是一個靜態(tài)方法即可。直接包裝一層:
public static int getInt(int resId) {return getInstance().getIntLite(resId); }為什么這里的getInstance()無參?因為LitePrefs構造方法是這樣的:
private LitePrefs() {}無參,什么也不做。對于這個類的初始化全都剝離到一個專門的初始化方法中去了。這意味著要使用這個類之前,必須先初始化。它們看上去像是這樣:
private boolean valid = false;public static void init(Context ctx) {getInstance().initLite(ctx); }public void initLite(Context ctx) {// do something to initialize valid = true; }private void checkValid() {if (!valid) {throw new IllegalStateException("this should only be called when LitePrefs didn't initialize once");}}記得用一個標志位來保障工具類已經(jīng)初始化過。
使用這種方式,所有的操作都可以簡化為LitePrefs.靜態(tài)方法()。
支持文件配置
完成之后,我們的Library會擁有這樣的初始化技能:
try {LitePrefs.initFromXml(context, R.xml.prefs);} catch (IOException | XmlPullParserException e) {e.printStackTrace();}支持文件配置不僅會讓配置變得很方便,同時也繞過了限制二:依常理考慮,一個設置項的默認值應該是惟一的。那么,如果在第一次啟動應用時寫一次初始值到SharedPreferences中,那么今后取值的時候不就永遠有值了嗎?那么上面那種單參封裝也就可以一直正常使用了。
既然要用文件讀寫,那就開搞吧,很容易想到使用一個xml文件來放配置項像是這樣:
<?xml version="1.0" encoding="utf-8"?> <prefs name="liteprefs"><pref><key>preference_key</key><def-value>default value</def-value><description>Write some sentences if you want,the LitePrefs parser will not parse the tag "description"</description></pref><pref><key>boolean_key</key><def-value>false</def-value></pref><pref><key>int_key</key><def-value>233</def-value></pref><pref><key>float_key</key><def-value>3.141592</def-value></pref><pref><key>long_key</key><def-value>4294967296</def-value></pref><pref><key>String_key</key><def-value>this is a String</def-value></pref> </prefs>由于xml解析器由我們自己來寫,所以非常自由。這里attribute"name"中寫上了對應的SharedPreferences使用的name。tag也是各種隨意。而且多寫幾個不解析的tag用來在配置文件中添加說明也沒有問題,像是上面的"<description>","</description>"。
基本數(shù)據(jù)類型全都可以很容易寫出來,處理也容易,就是Set<String>不是太好處理,但SharedPreferences中這個支持用到的場合還是非常少的,目前我在Android源碼中從未見過使用的例子。
考慮一個問題:上面怎么說也有五種類型的數(shù)據(jù),我們要怎么讀?只有兩個tag顯然不足以判斷這一項的具體類型是int還是String,難道我們要加一個tag專門來區(qū)分嗎?
雖然可以這樣做,但這樣寫model類又會是老大難的問題——要寫一個model類讓它持有標志類型的flag,再加上持有五種類型的域?這也太恐怖了吧!
話說回來,寫入配置到xml這一步真的是必要的嗎?
因為SharedPreferences要寫過之后才有值,所以我們想要在第一次運行應用時讀配置文件然后把值寫進xml,之后運行則不再需要進行這樣的操作——這就是原定計劃了,但這其實是存在漏洞的,漏洞出在SharedPreferences中的兩個方法上:remove(String key),clear()。
這兩個方法會把值清空,用戶來一發(fā)恢復默認設置的時候就是它們登場的時候。
既然如此,我們更改計劃:應用啟動時讀取配置文件并持有這些信息,在讀Preference項的時候,如該項未設置則返回配置文件中的默認值。
這樣一來,無須考慮寫文件操作的情況下,我們讀文件時條件也可放寬了:根本就不需要知道Preference的數(shù)據(jù)類型,全部用String類型保存就好,編程者為正確使用它們而負責。
我們用一個Pref類作為Preference項的模型,這樣設計:
public class Pref {public String key;/*** use String store the default value*/public String defValue;/*** use String store the current value*/public String curValue;/*** flag to show the pref has queried its data from SharedPreferences or not*/public boolean queried = false;public Pref() {}public Pref(String key, String defValue) {this.key = key;this.defValue = defValue;}public Pref(String key, int defValue) {this.key = key;this.defValue = String.valueOf(defValue);}.......public int getDefInt() {return Integer.parseInt(defValue);}public String getDefString() {return defValue;}.......public int getCurInt() {return Integer.parseInt(curValue);}public String getCurString() {return curValue;}.......public void setValue(int value) {curValue = String.valueOf(value);}public void setValue(String value) {curValue = value;}......以上代碼片段展示了對于int及String類型的處理,用一個defValue保存該Pref項的默認值;用queried標志是否該Pref曾經(jīng)進行過查詢,假如有,那么其實際值保存在curValue之中。通過這樣的處理,每一個Preference項最多只會查詢一次。
所以,解析器可以非常簡單地寫成像是這樣:
public class ParsePrefsXml {private static final String TAG_ROOT = "prefs";private static final String TAG_CHILD = "pref";private static final String ATTR_NAME = "name";private static final String TAG_KEY = "key";private static final String TAG_DEFAULT_VALUE = "def-value";public static ActualUtil parse(XmlResourceParser parser)throws XmlPullParserException, IOException {Map<String, Pref> map = new HashMap<>();int event = parser.getEventType();Pref pref = null;String name = null;Stack<String> tagStack = new Stack<>();while (event != XmlResourceParser.END_DOCUMENT) {if (event == XmlResourceParser.START_TAG) {switch (parser.getName()) {case TAG_ROOT:name = parser.getAttributeValue(null, ATTR_NAME);tagStack.push(TAG_ROOT);if (null == name) {throw new XmlPullParserException("Error in xml: doesn't contain a 'name' at line:"+ parser.getLineNumber());}break;case TAG_CHILD:pref = new Pref();tagStack.push(TAG_CHILD);break;case TAG_KEY:tagStack.push(TAG_KEY);break;case TAG_DEFAULT_VALUE:tagStack.push(TAG_DEFAULT_VALUE);break; // default: // throw new XmlPullParserException( // "Error in xml: tag isn't '" // + TAG_ROOT // + "' or '" // + TAG_CHILD // + "' or '" // + TAG_KEY // + "' or '" // + TAG_DEFAULT_VALUE // + "' at line:" // + parser.getLineNumber());}} else if (event == XmlResourceParser.TEXT) {switch (tagStack.peek()) {case TAG_KEY:pref.key = parser.getText();break;case TAG_DEFAULT_VALUE:pref.defValue = parser.getText();break;}} else if (event == XmlResourceParser.END_TAG) {boolean mismatch = false;switch (parser.getName()) {case TAG_ROOT:if (!TAG_ROOT.equals(tagStack.pop())) {mismatch = true;}break;case TAG_CHILD:if (!TAG_CHILD.equals(tagStack.pop())) {mismatch = true;}map.put(pref.key, pref);break;case TAG_KEY:if (!TAG_KEY.equals(tagStack.pop())) {mismatch = true;}break;case TAG_DEFAULT_VALUE:if (!TAG_DEFAULT_VALUE.equals(tagStack.pop())) {mismatch = true;}break;}if (mismatch) {throw new XmlPullParserException("Error in xml: mismatch end tag at line:"+ parser.getLineNumber());}}event = parser.next();}parser.close();return new ActualUtil(name, map);} }這里解析完成最后返回的ActualUtil是一個實際操作SharedPreferences的基礎工具類,它的邏輯也很簡單,像是這樣:
public class ActualUtil {private int editMode = LitePrefs.MODE_COMMIT;private String name;private SharedPreferences mSharedPreferences;private Map<String, Pref> mMap;public ActualUtil(String name, Map<String, Pref> map) {this.name = name;this.mMap = map;}public void init(Context context) {mSharedPreferences = context.getSharedPreferences(name, Context.MODE_PRIVATE);}public void setEditMode(int editMode) {this.editMode = editMode;}public void putToMap(String key, Pref pref) {mMap.put(key, pref);}private void checkExist(Pref pref) {if (null == pref) {throw new NullPointerException("operate a pref that isn't contained in data set,maybe there are some wrong in initialization of LitePrefs");}}private Pref readyOperation(String key) {Pref pref = mMap.get(key);checkExist(pref);return pref;}public int getInt(String key) {Pref pref = readyOperation(key);if (pref.queried) {return pref.getCurInt();} else {pref.queried = true;int ans = mSharedPreferences.getInt(key, pref.getDefInt());pref.setValue(ans);return ans;}}public boolean putInt(String key, int value) {Pref pref = readyOperation(key);pref.queried = true;pref.setValue(value);if (LitePrefs.MODE_APPLY == editMode) {mSharedPreferences.edit().putInt(key, value).apply();return true;}return mSharedPreferences.edit().putInt(key, value).commit();}...... }可擴展性
無擴展性、泛用性不夠的代碼只能作為一次性使用。
UML
我們的結構如圖中所示,ActualUtil持有SharedPreferences,實際完成讀寫操作,ParsePerfsXml提供解析方法將xml配置文件解析成相應的ActualUtil,而提供給用戶的實際操作類則為LitePrefs。
看上去抽象程度還算不錯,當我們需要針對項目特性定制的時候只需要繼承LitePrefs就可以……問題就出在這里,LitePrefs是個單例。
因為是單例,所以LitePrefs的構造方法為private,這保障了它不會在類外部被創(chuàng)建。但這也同時使得其無法派生出子類。這可不是一件好事。出于這個原由,我們特別設計一個不標準的單例BaseLitePrefs用于擴展:
private static volatile BaseLitePrefs sMe;protected BaseLitePrefs() {}public static BaseLitePrefs getInstance() {if (null == sMe) {synchronized (BaseLitePrefs.class) {if (null == sMe) {sMe = new BaseLitePrefs();}}}return sMe;}因為將訪問權限修改為了protected,所以這個類可以被順利繼承,雖然損失了一點嚴謹性,但這完全值得。
現(xiàn)在,我們可嘗試著寫一個子類看看:
public class MyLitePrefs extends BaseLitePrefs {public static final String THEME = "choose_theme_key";public static void initFromXml(Context context) {try {initFromXml(context, R.xml.prefs);} catch (IOException | XmlPullParserException e) {e.printStackTrace();}}public static ThemeUtils.Theme getTheme() {return ThemeUtils.Theme.mapValueToTheme(getInt(THEME));}public static boolean setTheme(int value) {return putInt(THEME, value);} }轉載于:https://www.cnblogs.com/holyday/p/7424104.html
總結
以上是生活随笔為你收集整理的使用SharedPreferences进行数据存储的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 百度地图API详解之公交导航
- 下一篇: -Java-泛型