Updated on 2016/1/26
歡迎轉(zhuǎn)載瞎嬉,但請(qǐng)保留作者鏈接:http://www.reibang.com/p/64ef6eb7406f
LitePreferences
完整源碼傳送門(mén)GitHub
開(kāi)局閑談
SharedPreferences是Android之中的基礎(chǔ)內(nèi)容霉囚,是一種非常輕量化的存儲(chǔ)工具殴边。核心思想就是在xml文件中保存鍵值對(duì)搪桂。而正因?yàn)椴捎玫氖俏募x寫(xiě)康谆,所以它天生線程不安全赐劣。Google曾經(jīng)想要對(duì)其進(jìn)行一番擴(kuò)展以令其實(shí)現(xiàn)線程安全讀寫(xiě)柄慰,但最終以失敗告終鳍悠。后來(lái)于是有了民間替代方案,詳細(xì)可以參考GitHub上這個(gè)項(xiàng)目坐搔。
筆者本身對(duì)SharedPreferences是否線程安全是沒(méi)有需求的藏研,我主要是覺(jué)得它——
限、制概行、太蠢挡、多!使凳忙、用业踏、太、麻涧卵、煩勤家!
吐槽及預(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類(lèi)型的情況,其他也是類(lèi)似柳恐。
以上就是SharedPreferences的基本使用情況了伐脖,足以應(yīng)付絕大部分情況,看上去也就那么幾行乐设,挺簡(jiǎn)單讼庇、挺好用的嘛!
那好伤提,我們現(xiàn)在來(lái)看一下它究竟有哪些短板巫俺。
限制之一,使用之前必須拿到Context:
// get it
SharedPreferences p = mContext.getSharedPreferences("Myprefs", Context.MODE_PRIVATE);
// or
p = PreferenceManager.getDefaultSharedPreferences(mContext);
這里展示了兩種方式肿男,第一種的優(yōu)勢(shì)是可以自定義名稱介汹,并且如果需要的話可以指定全局讀寫(xiě)(雖然Google不推薦用SharedPreferences來(lái)跨應(yīng)用讀寫(xiě)却嗡,相關(guān)字段早就被置上了deprecated),如果不需要?jiǎng)t純粹成了消耗多余體力的代碼嘹承。
而且窗价,Context并不是永遠(yuǎn)都那么好拿的,所以有一種最簡(jiǎn)單粗暴的作法就是做一個(gè)自己的Application類(lèi)像是這樣:
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í)之需撼港,拿來(lái)用SharedPreferences,每次還得這樣寫(xiě)App.getInstance()
骤竹,逼格太低又很累啊帝牡。
限制之二,讀值為什么會(huì)要這么多代碼:
// read
p.getString("preference_key", "default value");
初看上去蒙揣,這似乎是無(wú)比正常的代碼:"default value"的存在確保了你永遠(yuǎn)可以取到值靶溜,但問(wèn)題就出在這個(gè)"default value"上了,在某種情況下懒震,你需要取某個(gè)值的地方很多罩息,而且全都可能還沒(méi)有初始化過(guò),也就是說(shuō)在這些地方實(shí)際第一次處理時(shí)使用到值的是"default value"个扰,假如某一天"default value"值需要變更瓷炮,你就要細(xì)心謹(jǐn)慎地把每個(gè)地方都改一輪了。
限制之三递宅,寫(xiě)值代碼也很多:
// write
p.edit().putString("preference_key", "new value").commit();
// or
p.edit().putString("preference_key", "new value").apply();
先拿到Editor內(nèi)部類(lèi)娘香,再操作,最后再提交办龄,雖然IDE自帶補(bǔ)全功能茅主,但補(bǔ)全三次也不是那么方便吧?源碼中的說(shuō)法是土榴,“so you can chain put calls together.”
,因?yàn)槊看蝡utXXX()操作后仍舊返回同一個(gè)Editor內(nèi)部類(lèi)對(duì)象响牛,所以你能一次性put許多下最后再提交玷禽。可實(shí)際情況中使用到鏈?zhǔn)秸{(diào)用的機(jī)會(huì)還是挺少的呀打,畢竟很難出現(xiàn)Web上那種出現(xiàn)一整個(gè)表單給用戶填寫(xiě)矢赁,最后一次性提交的情況膨桥。
總的來(lái)說(shuō)芯勘,在不同的地方重復(fù)獲取SharedPreferences
是沒(méi)有必要的,可以拿一個(gè)單例來(lái)解決鬼佣;讀值和寫(xiě)值太累贅了豺憔,要做下封裝……
不额获,這還不夠够庙,作為一個(gè)名有追求的工程師——
我們需要一個(gè)強(qiáng)有力的Library來(lái)解決這些問(wèn)題,力爭(zhēng)達(dá)到一經(jīng)寫(xiě)就抄邀,永久受益的效果耘眨。
常規(guī)解決方案
一般是做一個(gè)單例工具類(lèi),然后簡(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è)類(lèi)已經(jīng)初始化過(guò)一次了,后面就可以這樣來(lái)獲取實(shí)例PreferenceUtils.getInstance(null)
——必須說(shuō)明這是一種取巧的手段环鲤,而且看上去非常丑陋——所以說(shuō)不需要依賴Context(另外我們還可以增加對(duì)于resId的支持纯趋,讓這種方式成為可能getStringParam(int resId)
只要在這個(gè)類(lèi)中持有Context就能做到——但要注意為防內(nèi)存泄漏應(yīng)給這個(gè)類(lèi)傳ApplicationContext);關(guān)鍵是限制二的解決并不漂亮楔绞,因?yàn)椴煌脑O(shè)置項(xiàng)的default值多數(shù)情況下是不一樣的结闸,所以還是提供了一個(gè)二參方法getStringParam(String key, String defaultString)
,本質(zhì)上并沒(méi)有解決酒朵。
不過(guò)不管怎樣桦锄,我們的Library LitePreferences
最起碼要包含以上這個(gè)工具類(lèi)的全部功能,然后再談突破蔫耽。
極致簡(jiǎn)約
既然是個(gè)單例结耀,那么在使用之前就必須調(diào)用getInstance()了,像是這樣:
LitePrefs.getInstance(mContext).getInt(R.string.tedious);
在這行代碼中匙铡,如果LitePrefs已經(jīng)初始化過(guò)一次了图甜,那么中間的getInstance(mContext)純粹就是毫無(wú)意義。我們希望代碼簡(jiǎn)約成這樣:
LitePrefs.getInt(R.string.tedious);
要達(dá)到這樣的效果鳖眼,只需讓getInt()是一個(gè)靜態(tài)方法即可黑毅。直接包裝一層:
public static int getInt(int resId) {
return getInstance().getIntLite(resId);
}
為什么這里的getInstance()無(wú)參?因?yàn)長(zhǎng)itePrefs構(gòu)造方法是這樣的:
private LitePrefs() {}
無(wú)參钦讳,什么也不做矿瘦。對(duì)于這個(gè)類(lèi)的初始化全都剝離到一個(gè)專(zhuān)門(mén)的初始化方法中去了。這意味著要使用這個(gè)類(lèi)之前愿卒,必須先初始化缚去。它們看上去像是這樣:
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)志位來(lái)保障工具類(lèi)已經(jīng)初始化過(guò)。
使用這種方式琼开,所有的操作都可以簡(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í)也繞過(guò)了限制二:依常理考慮搞动,一個(gè)設(shè)置項(xiàng)的默認(rèn)值應(yīng)該是惟一的躏精。那么,如果在第一次啟動(dòng)應(yīng)用時(shí)寫(xiě)一次初始值到SharedPreferences中滋尉,那么今后取值的時(shí)候不就永遠(yuǎn)有值了嗎玉控?那么上面那種單參封裝也就可以一直正常使用了。
既然要用文件讀寫(xiě)狮惜,那就開(kāi)搞吧高诺,很容易想到使用一個(gè)xml文件來(lái)放配置項(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解析器由我們自己來(lái)寫(xiě),所以非常自由碾篡。這里attribute
"name"中寫(xiě)上了對(duì)應(yīng)的SharedPreferences使用的name虱而。tag
也是各種隨意。而且多寫(xiě)幾個(gè)不解析的tag
用來(lái)在配置文件中添加說(shuō)明也沒(méi)有問(wèn)題开泽,像是上面的"<description>","</description>"牡拇。
基本數(shù)據(jù)類(lèi)型全都可以很容易寫(xiě)出來(lái),處理也容易穆律,就是Set<String>不是太好處理惠呼,但SharedPreferences中這個(gè)支持用到的場(chǎng)合還是非常少的,目前我在Android源碼中從未見(jiàn)過(guò)使用的例子峦耘。
考慮一個(gè)問(wèn)題:上面怎么說(shuō)也有五種類(lèi)型的數(shù)據(jù)剔蹋,我們要怎么讀?只有兩個(gè)tag顯然不足以判斷這一項(xiàng)的具體類(lèi)型是int還是String辅髓,難道我們要加一個(gè)tag專(zhuān)門(mén)來(lái)區(qū)分嗎泣崩?
雖然可以這樣做,但這樣寫(xiě)model類(lèi)又會(huì)是老大難的問(wèn)題——要寫(xiě)一個(gè)model類(lèi)讓它持有標(biāo)志類(lèi)型的flag洛口,再加上持有五種類(lèi)型的域矫付?這也太恐怖了吧!
話說(shuō)回來(lái)第焰,寫(xiě)入配置到xml這一步真的是必要的嗎买优?
因?yàn)?strong>SharedPreferences要寫(xiě)過(guò)之后才有值,所以我們想要在第一次運(yùn)行應(yīng)用時(shí)讀配置文件然后把值寫(xiě)進(jìn)xml挺举,之后運(yùn)行則不再需要進(jìn)行這樣的操作——這就是原定計(jì)劃了而叼,但這其實(shí)是存在漏洞的,漏洞出在SharedPreferences中的兩個(gè)方法上:remove(String key)
豹悬,clear()
。
這兩個(gè)方法會(huì)把值清空液荸,用戶來(lái)一發(fā)恢復(fù)默認(rèn)設(shè)置的時(shí)候就是它們登場(chǎng)的時(shí)候瞻佛。
既然如此,我們更改計(jì)劃:應(yīng)用啟動(dòng)時(shí)讀取配置文件并持有這些信息,在讀Preference項(xiàng)的時(shí)候伤柄,如該項(xiàng)未設(shè)置則返回配置文件中的默認(rèn)值绊困。
這樣一來(lái),無(wú)須考慮寫(xiě)文件操作的情況下适刀,我們讀文件時(shí)條件也可放寬了:根本就不需要知道Preference的數(shù)據(jù)類(lèi)型秤朗,全部用String類(lèi)型保存就好,編程者為正確使用它們而負(fù)責(zé)笔喉。
我們用一個(gè)Pref
類(lèi)作為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類(lèi)型的處理,用一個(gè)defValue
保存該P(yáng)ref項(xiàng)的默認(rèn)值常挚;用queried
標(biāo)志是否該P(yáng)ref曾經(jīng)進(jìn)行過(guò)查詢作谭,假如有,那么其實(shí)際值保存在curValue
之中奄毡。通過(guò)這樣的處理折欠,每一個(gè)Preference項(xiàng)最多只會(huì)查詢一次。
所以吼过,解析器可以非常簡(jiǎn)單地寫(xiě)成像是這樣:
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ǔ)工具類(lèi)锐秦,它的邏輯也很簡(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ò)展性
無(wú)擴(kuò)展性盗忱、泛用性不夠的代碼只能作為一次性使用酱床。
我們的結(jié)構(gòu)如圖中所示,ActualUtil持有SharedPreferences售淡,實(shí)際完成讀寫(xiě)操作斤葱,ParsePerfsXml提供解析方法將xml配置文件解析成相應(yīng)的ActualUtil,而提供給用戶的實(shí)際操作類(lèi)則為L(zhǎng)itePrefs揖闸。
看上去抽象程度還算不錯(cuò)揍堕,當(dāng)我們需要針對(duì)項(xiàng)目特性定制的時(shí)候只需要繼承LitePrefs就可以……問(wèn)題就出在這里,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)槭菃卫萌祝訪itePrefs的構(gòu)造方法為private,這保障了它不會(huì)在類(lèi)外部被創(chuàng)建贮泞。但這也同時(shí)使得其無(wú)法派生出子類(lèi)楞慈。這可不是一件好事。出于這個(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問(wèn)權(quán)限修改為了protected囊蓝,所以這個(gè)類(lèi)可以被順利繼承,雖然損失了一點(diǎn)嚴(yán)謹(jǐn)性令蛉,但這完全值得聚霜。
現(xiàn)在狡恬,我們可嘗試著寫(xiě)一個(gè)子類(lèi)看看:
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é)束,完整源碼鏈接在頂部蝎宇。