了解Android API中的SharedPreferences
Preference翻譯為偏好,但實際上它可以算是Setting(設置)的別名。兩種叫法給人的差異在于針對的對象不同:設置更傾向于設備的屬性,修改設置便是改變設備的功能;偏好則傾向于用戶的性格,用戶基于其個人的偏好來來形成設備的差異化。但是總體而言,它們是一致的,都是通過用戶的需求改變設備的體驗。
在Android的開發(fā)過程中,會在使用的API中見到很多名字中帶有Preference的類和接口,此篇文章就來介紹一下這些“*Prefere*”的功能和用途。
在Android提供API中,帶有Preference的其實主要分為兩類:一類是android.content包下的SharedPreferences,另一類則是android.preference包下的Preference。它們分別實現(xiàn)不同功能,卻又相互聯(lián)系合作完成對Android程序的控制。
SharedPreferences簡介
SharedPreferences是以復數(shù)形式存在,因為在Android中它是用來存儲鍵值對(Key-Value Pair)數(shù)據(jù)的集合。API中包含了多個方法來方面讀取相應類型的數(shù)據(jù):
- String getString(String key, String defValue);
- Set<String> getStringSet(String key, Set<String> defValues);
- int getInt(String key, int defValue);
- long getLong(String key, long defValue);
- float getFloat(String key, float defValue);
- boolean getBoolean(String key, boolean defValue);
這也表明SharedPreferences所能存儲的類型被限定在了String、int、long、float、boolean這些基礎數(shù)據(jù)類中,唯一的集合類型也只是Set,而它看起來更像是用來作為不能有重復數(shù)據(jù)的數(shù)組。
還可以單純檢查是否包換指定的主鍵,或者干脆將所有的鍵值對的Map獲取出來:
- boolean contains(String key);
- Map<String, ?> getAll();
Android系統(tǒng)的工程師在設計SharedPreferences的時候,把讀取的功能放在了SharedPreferences上,而把寫回的功能實現(xiàn)在了其內嵌的Editor類上,通過調用edit()方法來獲得一個寫入器。這樣就很容易實現(xiàn)一個只讀的對象,只要返回一個空指針或非可用的Editor對象就可以了。
- Editor putString(String key, String value);
- Editor putStringSet(String key, Set<String> values);
- Editor putInt(String key, int value);
- Editor putLong(String key, long value);
- Editor putFloat(String key, float value);
- Editor putBoolean(String key, boolean value);
- Editor remove(String key);
SharedPreferences還有一個內嵌接口OnSharedPreferenceChangeListener,實現(xiàn)它唯一的方法onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key)并通過以下方法添加在SharedPreferences對象上就可以監(jiān)聽其上鍵值對的增加、刪除和修改:
- void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
- void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
SharedPreferences的在Android系統(tǒng)中的實現(xiàn)
SharedPreferences和內嵌的Editor其實都只是接口定義而已,并沒有實現(xiàn)任何方法。它只是用來制定了一個存儲鍵值對的協(xié)議,具體的實現(xiàn)方式和存儲形式可以是任意的。在Android系統(tǒng)中,它默認以XML格式的文件來存儲這些數(shù)據(jù),實現(xiàn)的類則是SharedPreferencesImpl。
下邊就是所保存的XML文件的基本格式,它以數(shù)據(jù)類型作為XML元素的標簽,主鍵(key)是標簽name屬性的值,而主鍵對應的值則作為value屬性的值。但如果是String類型則作為標簽下的content,這樣就不用轉義引號也能更好的處理換行。另外對于null值存儲的結構也比較特殊,它以null為標簽,只有一個name屬性,沒有其他內容。
- <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
- <map>
- <string name="Name">Ider</string>
- <boolean name="Android" value="true" />
- <set name="Subsites">
- <string>code.iderzheng.com</string>
- <string>blog.iderzheng.com</string>
- <string>manual.iderzheng.com</string>
- </set>
- <int name="VersionCode" value="21" />
- <long name="VersionNumber" value="1355" />
- <float name="Version" value="5.0" />
- <null name="Null" />
- </map>
Android系統(tǒng)會把該XML文件存儲在/data/data/(packagename)/shared_prefs/下,每一個XML文件就對應一個SharedPreferences對象(實際是SharedPreferencesImpl對象)。但是SharedPreferences是接口不能用來實例化對象,而SharedPreferencesImpl是系統(tǒng)隱藏類,不能被直接訪問使用,其構造函數(shù)也只是包可見。所以不能通過new來構建一個SharedPreferences,必須通過Context提供的getSharedPreferences(String, int)來獲得實例。
該方法的***個參數(shù)是指定XML文件名(不包含“.xml”后綴)的字符串,方法會去讀取出對應的文件,創(chuàng)建一個SharedPreferences對象。第二個參數(shù)指定文件的訪問權限,共有4中可選模式,從API 17開始基于安全的考慮,MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE已經(jīng)被廢棄使用,只有MODE_PRIVATE和MODE_MULTI_PROCESS可使用,一般情況下指定MODE_PRIVATE即可。
對于從SharedPreferences中讀取指定主鍵的值是十分快的,因為所有存在XML的鍵值對信息全都被讀取被存儲在了SharedPreferences對象中的Map成員變量里,所以讀取都是基于內存訪問。使用Editor寫回到文件是避不開IO操作的,所以使用commit()提交修改還是會花費一些時間??紤]到這點,Android在API 9里引進了apply()方法來異步地將修改后的內容寫回到文件,當然在寫回前也會先更新內存中的鍵值對信息保證讀取到的時***的內容。
既然寫回可以是異步的,那么多次調用getSharedPreferences(String, int)獲得多個SharedPreferences賦值給不同的變量,假如一個變量做了修改,其他的對象不是會出現(xiàn)內容不一致的情況。其實這種情況并不會出現(xiàn),因為所有創(chuàng)建出來的SharedPreferences都被存儲在ContextImp的一個靜態(tài)成員變量中:
- /**
- * Map from package name, to preference name, to cached preferences.
- */
- private static ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>> sSharedPrefs;
這是一個從程序的Package名字到XML文件名再到SharedPreferences對象的二級Map。所以每次調用getSharedPreferences(String, int)得到的對象其實都是同一個實例,修改操作也都就作用在同一段內存中保證了所有訪問的一致性。apply()方法也會自動將所有修改排入隊列一一寫回文件從而不會因為順序的錯誤而造成意料之外的錯誤覆蓋。所以因為這個緩存機制的存在,多次調用getSharedPreferences(String, int)是非常效率的。而寫回時則推薦使用apply()實現(xiàn)異步操作,而不要用commit()阻礙主線程。
SharedPreferences的使用和示例
一般而言SharedPreferences的名字和主鍵名都是固定的,所以可以定義一些final的字符串變量來保存這些名字,在讀取和寫回時都使用這些常熟變量。對于之前展示的XML對應的代碼就如下邊所示:
- private static final String IDER_PREFERENCE = "ider-preference";
- private static final String IDER_PREFERENCE_KEY_NAME = "Name";
- private static final String IDER_PREFERENCE_KEY_SUBSITES = "Subsites";
- private static final String IDER_PREFERENCE_KEY_IS_ANDROID = "Android";
- private static final String IDER_PREFERENCE_KEY_VERSION = "Version";
- private static final String IDER_PREFERENCE_KEY_VERSION_CODE = "VersionCode";
- private static final String IDER_PREFERENCE_KEY_VERSION_NUMBER = "VersionNumber";
- private static final String IDER_PREFERENCE_KEY_NULL = "Null";
- public void write(Context context) {
- final SharedPreferences spref = context.getSharedPreferences(IDER_PREFERENCE, MODE_PRIVATE);
- HashSet<String> strs = new HashSet<String>();
- strs.add("blog.iderzheng.com");
- strs.add("code.iderzheng.com");
- strs.add("manual.iderzheng.com");
- SharedPreferences.Editor editor = spref.edit();
- editor.putString(IDER_PREFERENCE_KEY_NAME, "Ider");
- editor.putStringSet(IDER_PREFERENCE_KEY_SUBSITES, strs);
- editor.putBoolean(IDER_PREFERENCE_KEY_IS_ANDROID, true);
- editor.putFloat(IDER_PREFERENCE_KEY_VERSION, 5.0f);
- editor.putInt(IDER_PREFERENCE_KEY_VERSION_CODE, 21);
- editor.putLong(IDER_PREFERENCE_KEY_VERSION_NUMBER, 1355);
- editor.putString(IDER_PREFERENCE_KEY_NULL, null);
- editor.apply();
- }
- public void read(Context context) {
- final SharedPreferences spref = context.getSharedPreferences(IDER_PREFERENCE, MODE_PRIVATE);
- String name = spref.getString(IDER_PREFERENCE_KEY_NAME, "");
- Set<String> strs = spref.getStringSet(IDER_PREFERENCE_KEY_SUBSITES, null);
- boolean isAndroid = spref.getBoolean(IDER_PREFERENCE_KEY_IS_ANDROID, false);
- float version = spref.getFloat(IDER_PREFERENCE_KEY_VERSION, 0);
- int versionCode = spref.getInt(IDER_PREFERENCE_KEY_VERSION_CODE, 0);
- long versionNumber = spref.getLong(IDER_PREFERENCE_KEY_VERSION_NUMBER, 0);
- boolean hasKey = spref.contains(IDER_PREFERENCE_KEY_NULL);
- }
既然SharedPreferences的名字是可以任意給定的,所以對于SharedPreferences的使用就可以有非常的針對性創(chuàng)建不同的文件來存儲不同的內容。比如可以以不同用戶為名存放他們的偏好信息,可以根據(jù)不同界面保存布局信息、已訪問的頁碼。Activity就額外實現(xiàn)了獲取SharedPreferences的方法getPreferences(int),它只需要開發(fā)者提供文件的打開模式,自動以Activity的類名作為文件名。
SharedPreferences取值時是直接將給定主鍵在Map中的值強制轉換成所需要的值,所以如果用putInt存儲了整型卻用getBoolean()來取,并不會自動轉換成布爾型,而是直接拋出異常,所以要使用要注意保持類型一致。
另外如果沒有存儲過某個主鍵,SharedPreferences會返回null值,而對于String、Set這些類型同樣可以存儲null值,這樣就不能確定null是不是真是存儲的數(shù)據(jù)了。因此SharedPreferences還提供了contains (String key)方法來檢查給定的主鍵是真的存了null,還是因為并沒有這個鍵值對才返回的null。
SharedPreferences的優(yōu)缺點
之前講過所以讀取過的SharedPreferences的文件都會被緩存在Map里放在內存中,以便下次直接快速訪問,因為快事SharedPreferences的一大優(yōu)點。但是也因為都背緩存,而且存放格式是XML,所以SharedPreferences不宜存放過多的鍵值對,值的內容也不能太大。再者SharedPreferences只支持最基本的幾種類型,存儲一些用戶基本信息也足夠了。
如果對設備有root權限,那么就可以直接訪問/data/data/(packagename)/shared_prefs/目錄,將XML文件轉出來查看?;蛘咧苯佑迷赼db shell下直接用cat指令觀察數(shù)據(jù)的改變,非常的方便。
綜合而言,存儲一些內容較小、類型簡單的數(shù)據(jù),SharedPreferences絕對是***對象。