Updated on 2016/1/26
歡迎轉(zhuǎn)載,但請(qǐng)保留作者鏈接:http://m.itdecent.cn/p/64ef6eb7406f
LitePreferences完整源碼傳送門GitHub
開局閑談
SharedPreferences是Android之中的基礎(chǔ)內(nèi)容,是一種非常輕量化的存儲(chǔ)工具。核心思想就是在xml文件中保存鍵值對(duì)。而正因?yàn)椴捎玫氖俏募x寫,所以它天生線程不安全。Google曾經(jīng)想要對(duì)其進(jìn)行一番擴(kuò)展以令其實(shí)現(xiàn)線程安全讀寫,但最終以失敗告終。后來于是有了民間替代方案,詳細(xì)可以參考GitHub上這個(gè)項(xiàng)目。
筆者本身對(duì)SharedPreferences是否線程安全是沒有需求的,我主要是覺得它——
限、制、太、多!使、用、太、麻、煩!
吐槽及預(yù)期
// 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的基本使用情況了,足以應(yīng)付絕大部分情況,看上去也就那么幾行,挺簡(jiǎn)單、挺好用的嘛!
那好,我們現(xiàn)在來看一下它究竟有哪些短板。
限制之一,使用之前必須拿到Context:
// get it
SharedPreferences p = mContext.getSharedPreferences("Myprefs", Context.MODE_PRIVATE);
// or
p = PreferenceManager.getDefaultSharedPreferences(mContext);
這里展示了兩種方式,第一種的優(yōu)勢(shì)是可以自定義名稱,并且如果需要的話可以指定全局讀寫(雖然Google不推薦用SharedPreferences來跨應(yīng)用讀寫,相關(guān)字段早就被置上了deprecated),如果不需要?jiǎng)t純粹成了消耗多余體力的代碼。
而且,Context并不是永遠(yuǎn)都那么好拿的,所以有一種最簡(jiǎn)單粗暴的作法就是做一個(gè)自己的Application類像是這樣:
public class App extends Application {
private static Context sMe;
public static Context getInstance() {
return sMe;
}
@Override
public void onCreate() {
super.onCreate();
sMe = this;
}
}
但是殺雞焉用牛刀,你做這樣一個(gè)全局可得的ApplicationContext本就是為了不時(shí)之需,拿來用SharedPreferences,每次還得這樣寫App.getInstance(),逼格太低又很累啊。
限制之二,讀值為什么會(huì)要這么多代碼:
// read
p.getString("preference_key", "default value");
初看上去,這似乎是無比正常的代碼:"default value"的存在確保了你永遠(yuǎn)可以取到值,但問題就出在這個(gè)"default value"上了,在某種情況下,你需要取某個(gè)值的地方很多,而且全都可能還沒有初始化過,也就是說在這些地方實(shí)際第一次處理時(shí)使用到值的是"default value",假如某一天"default value"值需要變更,你就要細(xì)心謹(jǐn)慎地把每個(gè)地方都改一輪了。
限制之三,寫值代碼也很多:
// write
p.edit().putString("preference_key", "new value").commit();
// or
p.edit().putString("preference_key", "new value").apply();
先拿到Editor內(nèi)部類,再操作,最后再提交,雖然IDE自帶補(bǔ)全功能,但補(bǔ)全三次也不是那么方便吧?源碼中的說法是,“so you can chain put calls together.”,因?yàn)槊看蝡utXXX()操作后仍舊返回同一個(gè)Editor內(nèi)部類對(duì)象,所以你能一次性put許多下最后再提交??蓪?shí)際情況中使用到鏈?zhǔn)秸{(diào)用的機(jī)會(huì)還是挺少的,畢竟很難出現(xiàn)Web上那種出現(xiàn)一整個(gè)表單給用戶填寫,最后一次性提交的情況。
總的來說,在不同的地方重復(fù)獲取SharedPreferences是沒有必要的,可以拿一個(gè)單例來解決;讀值和寫值太累贅了,要做下封裝……
不,這還不夠,作為一個(gè)名有追求的工程師——
我們需要一個(gè)強(qiáng)有力的Library來解決這些問題,力爭(zhēng)達(dá)到一經(jīng)寫就,永久受益的效果。
常規(guī)解決方案
一般是做一個(gè)單例工具類,然后簡(jiǎn)單封裝一下方法,這里截取了一下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 @Singleton
protected 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();
}
......
}
可以看到其思想還是挺簡(jiǎn)單的,基本上對(duì)于限制一二三全都照顧到了。
對(duì)于限制一,因?yàn)槭菃卫?,只要明確這個(gè)類已經(jīng)初始化過一次了,后面就可以這樣來獲取實(shí)例PreferenceUtils.getInstance(null)——必須說明這是一種取巧的手段,而且看上去非常丑陋——所以說不需要依賴Context(另外我們還可以增加對(duì)于resId的支持,讓這種方式成為可能getStringParam(int resId)只要在這個(gè)類中持有Context就能做到——但要注意為防內(nèi)存泄漏應(yīng)給這個(gè)類傳ApplicationContext);關(guān)鍵是限制二的解決并不漂亮,因?yàn)椴煌脑O(shè)置項(xiàng)的default值多數(shù)情況下是不一樣的,所以還是提供了一個(gè)二參方法getStringParam(String key, String defaultString),本質(zhì)上并沒有解決。
不過不管怎樣,我們的Library LitePreferences最起碼要包含以上這個(gè)工具類的全部功能,然后再談突破。
極致簡(jiǎn)約
既然是個(gè)單例,那么在使用之前就必須調(diào)用getInstance()了,像是這樣:
LitePrefs.getInstance(mContext).getInt(R.string.tedious);
在這行代碼中,如果LitePrefs已經(jīng)初始化過一次了,那么中間的getInstance(mContext)純粹就是毫無意義。我們希望代碼簡(jiǎn)約成這樣:
LitePrefs.getInt(R.string.tedious);
要達(dá)到這樣的效果,只需讓getInt()是一個(gè)靜態(tài)方法即可。直接包裝一層:
public static int getInt(int resId) {
return getInstance().getIntLite(resId);
}
為什么這里的getInstance()無參?因?yàn)長(zhǎng)itePrefs構(gòu)造方法是這樣的:
private LitePrefs() {}
無參,什么也不做。對(duì)于這個(gè)類的初始化全都剝離到一個(gè)專門的初始化方法中去了。這意味著要使用這個(gè)類之前,必須先初始化。它們看上去像是這樣:
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");
}
}
記得用一個(gè)標(biāo)志位來保障工具類已經(jīng)初始化過。
使用這種方式,所有的操作都可以簡(jiǎn)化為L(zhǎng)itePrefs.靜態(tài)方法()。
支持文件配置
完成之后,我們的Library會(huì)擁有這樣的初始化技能:
try {
LitePrefs.initFromXml(context, R.xml.prefs);
} catch (IOException | XmlPullParserException e) {
e.printStackTrace();
}
支持文件配置不僅會(huì)讓配置變得很方便,同時(shí)也繞過了限制二:依常理考慮,一個(gè)設(shè)置項(xiàng)的默認(rèn)值應(yīng)該是惟一的。那么,如果在第一次啟動(dòng)應(yīng)用時(shí)寫一次初始值到SharedPreferences中,那么今后取值的時(shí)候不就永遠(yuǎn)有值了嗎?那么上面那種單參封裝也就可以一直正常使用了。
既然要用文件讀寫,那就開搞吧,很容易想到使用一個(gè)xml文件來放配置項(xiàng)像是這樣:
<?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"中寫上了對(duì)應(yīng)的SharedPreferences使用的name。tag也是各種隨意。而且多寫幾個(gè)不解析的tag用來在配置文件中添加說明也沒有問題,像是上面的"<description>","</description>"。
基本數(shù)據(jù)類型全都可以很容易寫出來,處理也容易,就是Set<String>不是太好處理,但SharedPreferences中這個(gè)支持用到的場(chǎng)合還是非常少的,目前我在Android源碼中從未見過使用的例子。
考慮一個(gè)問題:上面怎么說也有五種類型的數(shù)據(jù),我們要怎么讀?只有兩個(gè)tag顯然不足以判斷這一項(xiàng)的具體類型是int還是String,難道我們要加一個(gè)tag專門來區(qū)分嗎?
雖然可以這樣做,但這樣寫model類又會(huì)是老大難的問題——要寫一個(gè)model類讓它持有標(biāo)志類型的flag,再加上持有五種類型的域?這也太恐怖了吧!
話說回來,寫入配置到xml這一步真的是必要的嗎?
因?yàn)?strong>SharedPreferences要寫過之后才有值,所以我們想要在第一次運(yùn)行應(yīng)用時(shí)讀配置文件然后把值寫進(jìn)xml,之后運(yùn)行則不再需要進(jìn)行這樣的操作——這就是原定計(jì)劃了,但這其實(shí)是存在漏洞的,漏洞出在SharedPreferences中的兩個(gè)方法上:remove(String key),clear()。
這兩個(gè)方法會(huì)把值清空,用戶來一發(fā)恢復(fù)默認(rèn)設(shè)置的時(shí)候就是它們登場(chǎng)的時(shí)候。
既然如此,我們更改計(jì)劃:應(yīng)用啟動(dòng)時(shí)讀取配置文件并持有這些信息,在讀Preference項(xiàng)的時(shí)候,如該項(xiàng)未設(shè)置則返回配置文件中的默認(rèn)值。
這樣一來,無須考慮寫文件操作的情況下,我們讀文件時(shí)條件也可放寬了:根本就不需要知道Preference的數(shù)據(jù)類型,全部用String類型保存就好,編程者為正確使用它們而負(fù)責(zé)。
我們用一個(gè)Pref類作為Preference項(xiàng)的模型,這樣設(shè)計(jì):
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;
}
......
以上代碼片段展示了對(duì)于int及String類型的處理,用一個(gè)defValue保存該P(yáng)ref項(xiàng)的默認(rèn)值;用queried標(biāo)志是否該P(yáng)ref曾經(jīng)進(jìn)行過查詢,假如有,那么其實(shí)際值保存在curValue之中。通過這樣的處理,每一個(gè)Preference項(xiàng)最多只會(huì)查詢一次。
所以,解析器可以非常簡(jiǎn)單地寫成像是這樣:
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是一個(gè)實(shí)際操作SharedPreferences的基礎(chǔ)工具類,它的邏輯也很簡(jiǎn)單,像是這樣:
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();
}
......
}
可擴(kuò)展性
無擴(kuò)展性、泛用性不夠的代碼只能作為一次性使用。

我們的結(jié)構(gòu)如圖中所示,ActualUtil持有SharedPreferences,實(shí)際完成讀寫操作,ParsePerfsXml提供解析方法將xml配置文件解析成相應(yīng)的ActualUtil,而提供給用戶的實(shí)際操作類則為L(zhǎng)itePrefs。
看上去抽象程度還算不錯(cuò),當(dāng)我們需要針對(duì)項(xiàng)目特性定制的時(shí)候只需要繼承LitePrefs就可以……問題就出在這里,LitePrefs是個(gè)單例。
private static volatile LitePrefs sMe;
private LitePrefs() {
}
public static LitePrefs getInstance() {
if (null == sMe) {
synchronized (LitePrefs.class) {
if (null == sMe) {
sMe = new LitePrefs();
}
}
}
return sMe;
}
因?yàn)槭菃卫?,所以LitePrefs的構(gòu)造方法為private,這保障了它不會(huì)在類外部被創(chuàng)建。但這也同時(shí)使得其無法派生出子類。這可不是一件好事。出于這個(gè)原由,我們特別設(shè)計(jì)一個(gè)不標(biāo)準(zhǔn)的單例BaseLitePrefs用于擴(kuò)展:
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;
}
因?yàn)閷⒃L問權(quán)限修改為了protected,所以這個(gè)類可以被順利繼承,雖然損失了一點(diǎn)嚴(yán)謹(jǐn)性,但這完全值得。
現(xiàn)在,我們可嘗試著寫一個(gè)子類看看:
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);
}
}
本篇至此結(jié)束,完整源碼鏈接在頂部。