「打造自己的Library」SharedPreferences篇
Updated on 2016/1/26
欢迎转载,但请保留作者链接:http://www.jianshu.com/p/64ef6eb7406f
LitePreferences
完整源码传送门GitHub
开局闲谈
SharedPreferences是Android之中的基础内容,是一种非常轻量化的存储工具。核心思想就是在xml文件中保存键值对。而正因为采用的是文件读写,所以它天生线程不安全。Google曾经想要对其进行一番扩展以令其实现线程安全读写,但最终以失败告终。后来于是有了民间替代方案,详细可以参考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的基本使用情况了,足以应付绝大部分情况,看上去也就那么几行,挺简单、挺好用的嘛!
那好,我们现在来看一下它究竟有哪些短板。
限制之一,使用之前必须拿到Context:
// get it
SharedPreferences p = mContext.getSharedPreferences("Myprefs", Context.MODE_PRIVATE);
// or
p = PreferenceManager.getDefaultSharedPreferences(mContext);
这里展示了两种方式,第一种的优势是可以自定义名称,并且如果需要的话可以指定全局读写(虽然Google不推荐用SharedPreferences来跨应用读写,相关字段早就被置上了deprecated),如果不需要则纯粹成了消耗多余体力的代码。
而且,Context并不是永远都那么好拿的,所以有一种最简单粗暴的作法就是做一个自己的Application类像是这样:
public class App extends Application {
private static Context sMe;
public static Context getInstance() {
return sMe;
}
@Override
public void onCreate() {
super.onCreate();
sMe = this;
}
}
但是杀鸡焉用牛刀,你做这样一个全局可得的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许多下最后再提交。可实际情况中使用到链式调用的机会还是挺少的,毕竟很难出现Web上那种出现一整个表单给用户填写,最后一次性提交的情况。
总的来说,在不同的地方重复获取SharedPreferences
是没有必要的,可以拿一个单例来解决;读值和写值太累赘了,要做下封装……
不,这还不够,作为一个名有追求的工程师——
我们需要一个强有力的Library来解决这些问题,力争达到一经写就,永久受益的效果。
常规解决方案
一般是做一个单例工具类,然后简单封装一下方法,这里截取了一下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();
}
......
}
可以看到其思想还是挺简单的,基本上对于限制一二三全都照顾到了。
对于限制一,因为是单例,只要明确这个类已经初始化过一次了,后面就可以这样来获取实例PreferenceUtils.getInstance(null)
——必须说明这是一种取巧的手段,而且看上去非常丑陋——所以说不需要依赖Context(另外我们还可以增加对于resId的支持,让这种方式成为可能getStringParam(int resId)
只要在这个类中持有Context就能做到——但要注意为防内存泄漏应给这个类传ApplicationContext);关键是限制二的解决并不漂亮,因为不同的设置项的default值多数情况下是不一样的,所以还是提供了一个二参方法getStringParam(String key, String defaultString)
,本质上并没有解决。
不过不管怎样,我们的Library LitePreferences
最起码要包含以上这个工具类的全部功能,然后再谈突破。
极致简约
既然是个单例,那么在使用之前就必须调用getInstance()了,像是这样:
LitePrefs.getInstance(mContext).getInt(R.string.tedious);
在这行代码中,如果LitePrefs已经初始化过一次了,那么中间的getInstance(mContext)纯粹就是毫无意义。我们希望代码简约成这样:
LitePrefs.getInt(R.string.tedious);
要达到这样的效果,只需让getInt()是一个静态方法即可。直接包装一层:
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");
}
}
记得用一个标志位来保障工具类已经初始化过。
使用这种方式,所有的操作都可以简化为LitePrefs.静态方法()。
支持文件配置
完成之后,我们的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>"。
基本数据类型全都可以很容易写出来,处理也容易,就是Set<String>不是太好处理,但SharedPreferences中这个支持用到的场合还是非常少的,目前我在Android源码中从未见过使用的例子。
考虑一个问题:上面怎么说也有五种类型的数据,我们要怎么读?只有两个tag显然不足以判断这一项的具体类型是int还是String,难道我们要加一个tag专门来区分吗?
虽然可以这样做,但这样写model类又会是老大难的问题——要写一个model类让它持有标志类型的flag,再加上持有五种类型的域?这也太恐怖了吧!
话说回来,写入配置到xml这一步真的是必要的吗?
因为SharedPreferences要写过之后才有值,所以我们想要在第一次运行应用时读配置文件然后把值写进xml,之后运行则不再需要进行这样的操作——这就是原定计划了,但这其实是存在漏洞的,漏洞出在SharedPreferences中的两个方法上:remove(String key)
,clear()
。
这两个方法会把值清空,用户来一发恢复默认设置的时候就是它们登场的时候。
既然如此,我们更改计划:应用启动时读取配置文件并持有这些信息,在读Preference项的时候,如该项未设置则返回配置文件中的默认值。
这样一来,无须考虑写文件操作的情况下,我们读文件时条件也可放宽了:根本就不需要知道Preference的数据类型,全部用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曾经进行过查询,假如有,那么其实际值保存在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();
}
......
}
可扩展性
无扩展性、泛用性不够的代码只能作为一次性使用。
我们的结构如图中所示,ActualUtil持有SharedPreferences,实际完成读写操作,ParsePerfsXml提供解析方法将xml配置文件解析成相应的ActualUtil,而提供给用户的实际操作类则为LitePrefs。
看上去抽象程度还算不错,当我们需要针对项目特性定制的时候只需要继承LitePrefs就可以……问题就出在这里,LitePrefs是个单例。
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;
}
因为是单例,所以LitePrefs的构造方法为private,这保障了它不会在类外部被创建。但这也同时使得其无法派生出子类。这可不是一件好事。出于这个原由,我们特别设计一个不标准的单例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,所以这个类可以被顺利继承,虽然损失了一点严谨性,但这完全值得。
现在,我们可尝试着写一个子类看看:
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);
}
}
本篇至此结束,完整源码链接在顶部。