「打造自己的Library」SharedPreferences篇

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ò)展性、泛用性不夠的代碼只能作為一次性使用。

UML

我們的結(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é)束,完整源碼鏈接在頂部。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,697評(píng)論 19 139
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,853評(píng)論 18 399
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,351評(píng)論 25 708
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 47,290評(píng)論 6 342
  • 從三月份找實(shí)習(xí)到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
    時(shí)芥藍(lán)閱讀 42,886評(píng)論 11 349

友情鏈接更多精彩內(nèi)容