Preference翻译为偏好,但实际上它可以算是Setting(设置)的别名。两种叫法给人的差异在于针对的对象不同:设置更倾向于设备的属性,修改设置便是改变设备的功能;偏好则倾向于用户的性格,用户基于其个人的偏好来来形成设备的差异化。但是总体而言,它们是一致的,都是通过用户的需求改变设备的体验。
在Android的开发过程中,会在使用的API中见到很多名字中带有Preference的类和接口,此篇文章就来介绍一下这些“*Prefere*”的功能和用途。
在Android提供API中,带有Preference的其实主要分为两类:一类是android.content包下的SharedPreferences,另一类则是android.preference包下的Preference。它们分别实现不同功能,却又相互联系合作完成对Android程序的控制。
SharedPreferences简介
SharedPreferences是以复数形式存在,因为在Android中它是用来存储键值对(Key-Value Pair)数据的集合。API中包含了多个方法来方面读取相应类型的数据:
- 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获取出来:
- boolean contains(String key);
- Map<String, ?> getAll();
Android系统的工程师在设计SharedPreferences的时候,把读取的功能放在了SharedPreferences上,而把写回的功能实现在了其内嵌的Editor类上,通过调用edit()方法来获得一个写入器。这样就很容易实现一个只读的对象,只要返回一个空指针或非可用的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,实现它唯一的方法onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key)并通过以下方法添加在SharedPreferences对象上就可以监听其上键值对的增加、删除和修改:
- 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属性,没有其他内容。
- <?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的一个静态成员变量中:
- /**
- * 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对应的代码就如下边所示:
- 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绝对是***对象。