Preference翻譯為偏好,但實際上它可以算是Setting(設定)的別名。兩種叫法給人的差異在於針對的物件不同:設定更傾向於裝置的屬性,修改設定便是改變裝置的功能;偏好則傾向於使用者的性格,使用者基於其個人的偏好來來形成裝置的差異化。但是總體而言,它們是一致的,都是通過使用者的需求改變裝置的體驗。
在Android的開發過程中,會在使用的API中見到很多名字中帶有Preference的類和介面,此篇文章就來介紹一下這些“*Prefere*
”的功能和用途。
在Android提供API中,帶有Preference的其實主要分為兩類:一類是android.content
包下的SharedPreferences
,另一類則是android.preference
包下的Preference
。它們分別實現不同功能,卻又相互聯絡合作完成對Android程式的控制。
SharedPreferences簡介
SharedPreferences
是以複數形式存在,因為在Android中它是用來儲存鍵值對(Key-Value Pair)資料的集合。API中包含了多個方法來方面讀取相應型別的資料:
1 2 3 4 5 6 |
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
這些基礎資料類中,唯一的集合型別也只是Set
,而它看起來更像是用來作為不能有重複資料的陣列。
還可以單純檢查是否包換指定的主鍵,或者乾脆將所有的鍵值對的Map
獲取出來:
1 2 |
boolean contains(String key); Map<String, ?> getAll(); |
Android系統的工程師在設計SharedPreferences
的時候,把讀取的功能放在了SharedPreferences
上,而把寫回的功能實現在了其內嵌的Editor
類上,通過呼叫edit()
方法來獲得一個寫入器。這樣就很容易實現一個只讀的物件,只要返回一個空指標或非可用的Editor物件就可以了。
1 2 3 4 5 6 7 |
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
,實現它唯一的方法onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key)
並通過以下方法新增在SharedPreferences
物件上就可以監聽其上鍵值對的增加、刪除和修改:
1 2 |
void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener); void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener); |
SharedPreferences的在Android系統中的實現
SharedPreferences
和內嵌的Editor
其實都只是介面定義而已,並沒有實現任何方法。它只是用來制定了一個儲存鍵值對的協議,具體的實現方式和儲存形式可以是任意的。在Android系統中,它預設以XML格式的檔案來儲存這些資料,實現的類則是SharedPreferencesImpl
。
下邊就是所儲存的XML檔案的基本格式,它以資料型別作為XML元素的標籤,主鍵(key)是標籤name屬性的值,而主鍵對應的值則作為value屬性的值。但如果是String型別則作為標籤下的content,這樣就不用轉義引號也能更好的處理換行。另外對於null
值儲存的結構也比較特殊,它以null
為標籤,只有一個name
屬性,沒有其他內容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?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系統會把該XML檔案儲存在/data/data/(packagename)/shared_prefs/
下,每一個XML檔案就對應一個SharedPreferences
物件(實際是SharedPreferencesImpl
物件)。但是SharedPreferences
是介面不能用來例項化物件,而SharedPreferencesImpl
是系統隱藏類,不能被直接訪問使用,其建構函式也只是包可見。所以不能通過new來構建一個SharedPreferences
,必須通過Context
提供的getSharedPreferences(String, int)
來獲得例項。
該方法的第一個引數是指定XML檔名(不包含“.xml”字尾)的字串,方法會去讀取出對應的檔案,建立一個SharedPreferences
物件。第二個引數指定檔案的訪問許可權,共有4中可選模式,從API 17開始基於安全的考慮,MODE_WORLD_READABLE
和MODE_WORLD_WRITEABLE
已經被廢棄使用,只有MODE_PRIVATE
和MODE_MULTI_PROCESS
可使用,一般情況下指定MODE_PRIVATE
即可。
對於從SharedPreferences
中讀取指定主鍵的值是十分快的,因為所有存在XML的鍵值對資訊全都被讀取被儲存在了SharedPreferences
物件中的Map
成員變數裡,所以讀取都是基於記憶體訪問。使用Editor
寫回到檔案是避不開IO操作的,所以使用commit()
提交修改還是會花費一些時間。考慮到這點,Android在API 9裡引進了apply()
方法來非同步地將修改後的內容寫回到檔案,當然在寫回前也會先更新記憶體中的鍵值對資訊保證讀取到的時最新的內容。
既然寫回可以是非同步的,那麼多次呼叫getSharedPreferences(String, int)
獲得多個SharedPreferences
賦值給不同的變數,假如一個變數做了修改,其他的物件不是會出現內容不一致的情況。其實這種情況並不會出現,因為所有建立出來的SharedPreferences
都被儲存在ContextImp
的一個靜態成員變數中:
1 2 3 4 |
/** * 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()
實現非同步操作,而不要用commit()
阻礙主執行緒。
SharedPreferences的使用和示例
一般而言SharedPreferences
的名字和主鍵名都是固定的,所以可以定義一些final
的字串變數來儲存這些名字,在讀取和寫回時都使用這些常熟變數。對於之前展示的XML對應的程式碼就如下邊所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
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
的使用就可以有非常的針對性建立不同的檔案來儲存不同的內容。比如可以以不同使用者為名存放他們的偏好資訊,可以根據不同介面儲存佈局資訊、已訪問的頁碼。Activity
就額外實現了獲取SharedPreferences
的方法getPreferences(int)
,它只需要開發者提供檔案的開啟模式,自動以Activity
的類名作為檔名。
SharedPreferences
取值時是直接將給定主鍵在Map中的值強制轉換成所需要的值,所以如果用putInt儲存了整型卻用getBoolean()來取,並不會自動轉換成布林型,而是直接丟擲異常,所以要使用要注意保持型別一致。
另外如果沒有儲存過某個主鍵,SharedPreferences
會返回null
值,而對於String
、Set
這些型別同樣可以儲存null
值,這樣就不能確定null
是不是真是儲存的資料了。因此SharedPreferences
還提供了contains (String key)
方法來檢查給定的主鍵是真的存了null
,還是因為並沒有這個鍵值對才返回的null
。
SharedPreferences的優缺點
之前講過所以讀取過的SharedPreferences
的檔案都會被快取在Map
裡放在記憶體中,以便下次直接快速訪問,因為快事SharedPreferences
的一大優點。但是也因為都背快取,而且存放格式是XML,所以SharedPreferences
不宜存放過多的鍵值對,值的內容也不能太大。再者SharedPreferences
只支援最基本的幾種型別,儲存一些使用者基本資訊也足夠了。
如果對裝置有root許可權,那麼就可以直接訪問/data/data/(packagename)/shared_prefs/
目錄,將XML檔案轉出來檢視。或者直接用在adb shell下直接用cat
指令觀察資料的改變,非常的方便。
綜合而言,儲存一些內容較小、型別簡單的資料,SharedPreferences
絕對是首選物件。
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!