美文网首页安卓进阶利器程序员技术文
「打造自己的Library」SharedPreferences篇

「打造自己的Library」SharedPreferences篇

作者: 郭非文 | 来源:发表于2016-01-06 21:12 被阅读4468次

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

    可扩展性

    无扩展性、泛用性不够的代码只能作为一次性使用。

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

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

    相关文章

      网友评论

      • 微风_1707:有一个疑问,这种写法是在外部建立一个map把要保存在SharedPreferences的值和默认值都在map里保存了一份,而SharedPreferences在读写存取的时候也会在内存中建立一个类似的map来保存键值对,这样的写法会不会增加不必要的内存消耗?
      • 追梦者king:太牛了,这个shareperference是把东西写到xml里?

      本文标题:「打造自己的Library」SharedPreferences篇

      本文链接:https://www.haomeiwen.com/subject/pwkohttx.html