「打造自己的Library」SharedPreferences篇
发布日期:2021-11-09 22:50:57 浏览次数:41 分类:技术文章

本文共 15373 字,大约阅读时间需要 51 分钟。

Updated on 2016/1/26

LitePreferences完整源码传送门

开局闲谈

SharedPreferences是Android之中的基础内容,是一种非常轻量化的存储工具。核心思想就是在xml文件中保存键值对。而正因为采用的是文件读写,所以它天生线程不安全。Google曾经想要对其进行一番扩展以令其实现线程安全读写,但最终以失败告终。后来于是有了民间替代方案,详细可以参考GitHub上这个。

笔者本身对SharedPreferences是否线程安全是没有需求的,我主要是觉得它——
限、制、太、多!使、用、太、麻、烦!

吐槽及预期

// get itSharedPreferences p = mContext.getSharedPreferences("Myprefs", Context.MODE_PRIVATE);// orp = PreferenceManager.getDefaultSharedPreferences(mContext);// readp.getString("preference_key", "default value");// writep.edit().putString("preference_key", "new value").commit();// orp.edit().putString("preference_key", "new value").apply();

这里演示了String类型的情况,其他也是类似。

以上就是SharedPreferences的基本使用情况了,足以应付绝大部分情况,看上去也就那么几行,挺简单、挺好用的嘛!
那好,我们现在来看一下它究竟有哪些短板。

限制之一,使用之前必须拿到Context:

// get itSharedPreferences p = mContext.getSharedPreferences("Myprefs", Context.MODE_PRIVATE);// orp = 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(),逼格太低又很累啊。

限制之二,读值为什么会要这么多代码:

// readp.getString("preference_key", "default value");

初看上去,这似乎是无比正常的代码:”default value”的存在确保了你永远可以取到值,但问题就出在这个”default value”上了,在某种情况下,你需要取某个值的地方很多,而且全都可能还没有初始化过,也就是说在这些地方实际第一次处理时使用到值的是”default value”,假如某一天”default value”值需要变更,你就要细心谨慎地把每个地方都改一轮了。

限制之三,写值代码也很多:

// writep.edit().putString("preference_key", "new value").commit();// orp.edit().putString("preference_key", "new value").apply();

先拿到Editor内部类,再操作,最后再提交,虽然IDE自带补全功能,但补全三次也不是那么方便吧?源码中的说法是,“so you can chain put calls together.”,因为每次putXXX()操作后仍旧返回同一个Editor内部类对象,所以你能一次性put许多下最后再提交。可实际情况中使用到链式调用的机会还是挺少的,毕竟很难出现Web上那种出现一整个表单给用户填写,最后一次性提交的情况。

总的来说,在不同的地方重复获取SharedPreferences是没有必要的,可以拿一个单例来解决;读值和写值太累赘了,要做下封装……

不,这还不够,作为一个名有追求的工程师——
我们需要一个强有力的Library来解决这些问题,力争达到一经写就,永久受益的效果。

常规解决方案

一般是做一个单例工具类,然后简单封装一下方法,这里截取了一下中的部分代码如下:

/** * 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文件来放配置项像是这样:

preference_key
default value
Write some sentences if you want, the LitePrefs parser will not parse the tag "description"
boolean_key
false
int_key
233
float_key
3.141592
long_key
4294967296
String_key
this is a String

由于xml解析器由我们自己来写,所以非常自由。这里attribute“name”中写上了对应的SharedPreferences使用的name。tag也是各种随意。而且多写几个不解析的tag用来在配置文件中添加说明也没有问题,像是上面的”","“。

基本数据类型全都可以很容易写出来,处理也容易,就是Set不是太好处理,但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
map = new HashMap<>(); int event = parser.getEventType(); Pref pref = null; String name = null; Stack
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
mMap; public ActualUtil(String name, Map
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是个单例

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); }}

本篇至此结束,完整源码链接在顶部。

转载地址:https://blog.csdn.net/dbnight/article/details/50688332 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:Handler官方范例AsyncQueryHandler源码解析
下一篇:光速上手Shell——简单批量文件操作为例

发表评论

最新留言

留言是一种美德,欢迎回访!
[***.207.175.100]2024年04月17日 20时06分46秒