Preference翻译为偏好,但实际上它可以算是Setting(设置)的别名。两种叫法给人的差异在于针对的对象不同:设置更倾向于设备的属性,修改设置便是改变设备的功能;偏好则倾向于用户的性格,用户基于其个人的偏好来来形成设备的差异化。但是总体而言,他们是一致的,都是通过用户的需求改变设备的体验。
在Android的开发过程中,会在使用的API中见到很多名字中带有Preference的类和接口,此篇文章就来介绍一下这些“*Prefere*
”的功能和用途。
在Android提供API中,带有Preference的其实主要分为两类:一类是android.content
包下的SharedPreferences
,另一类则是android.preference
包下的Preference
。它们分别实现不同功能,却又相互联系合作完成对Android程序的控制。
SharedPreferences简介
SharedPreferences
是以复数形式存在,因为在Android中它是用来存储键值对(Key-Value Pair)数据的集合。API中包含了多个方法来方面读取相应类型的数据:
[codesyntax lang=”java” lines=”normal”]
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);
[/codesyntax]
这也表明SharedPreferences
所能存储的类型被限定在了String
、int
、long
、float
、boolean
这些基础数据类中,唯一的集合类型也只是Set<String>
,而它看起来更像是用来作为不能有重复数据的数组。
还可以单纯检查是否包换指定的主键,或者干脆将所有的键值对的Map
获取出来:
[codesyntax lang=”java” lines=”normal”]
boolean contains(String key); Map<String, ?> getAll();
[/codesyntax]
Android系统的工程师在设计SharedPreferences
的时候,把读取的功能放在了SharedPreferences
上,而把写回的功能实现在了其内嵌的Editor
类上,通过调用edit()
方法来获得一个写入器。这样就很容易实现一个只读的对象,只要返回一个空指针或非可用的Editor对象就可以了。
[codesyntax lang=”java” lines=”normal”]
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);
[/codesyntax]
SharedPreferences
还有一个内嵌接口OnSharedPreferenceChangeListener
,实现它唯一的方法onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key)
并通过以下方法添加在SharedPreferences
对象上就可以监听其上键值对的增加、删除和修改:
[codesyntax lang=”java” lines=”normal”]
void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener); void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
[/codesyntax]
SharedPreferences的在Android系统中的实现
SharedPreferences
和内嵌的Editor
其实都只是接口定义而已,并没有实现任何方法。它只是用来制定了一个存储键值对的协议,具体的实现方式和存储形式可以是任意的。在Android系统中,它默认以XML格式的文件来存储这些数据,实现的类则是SharedPreferencesImpl
。
下边就是所保存的XML文件的基本格式,它以数据类型作为XML元素的标签,主键(key)是标签name属性的值,而主键对应的值则作为value属性的值。但如果是String类型则作为标签下的content,这样就不用转义引号也能更好的处理换行。另外对于null
值存储的结构也比较特殊,它以null
为标签,只有一个name
属性,没有其他内容。
[codesyntax lang=”xml” lines=”normal”]
<?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>
[/codesyntax]
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<String, Object>
成员变量里,所以读取都是基于内存访问。使用Editor
写回到文件是避不开IO操作的,所以使用commit()
提交修改还是会花费一些时间。考虑到这点,Android在API 9里引进了apply()
方法来异步地将修改后的内容写回到文件,当然在写回前也会先更新内存中的键值对信息保证读取到的时最新的内容。
既然写回可以是异步的,那么多次调用getSharedPreferences(String, int)
获得多个SharedPreferences
赋值给不同的变量,假如一个变量做了修改,其他的对象不是会出现内容不一致的情况。其实这种情况并不会出现,因为所有创建出来的SharedPreferences
都被存储在ContextImp
的一个静态成员变量中:
[codesyntax lang=”java” lines=”normal”]
/** * Map from package name, to preference name, to cached preferences. */ private static ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>> sSharedPrefs;
[/codesyntax]
这是一个从程序的Package名字到XML文件名再到SharedPreferences
对象的二级Map
。所以每次调用getSharedPreferences(String, int)
得到的对象其实都是同一个实例,修改操作也都就作用在同一段内存中保证了所有访问的一致性。apply()
方法也会自动将所有修改排入队列一一写回文件从而不会因为顺序的错误而造成意料之外的错误覆盖。所以因为这个缓存机制的存在,多次调用getSharedPreferences(String, int)
是非常效率的。而写回时则推荐使用apply()
实现异步操作,而不要用commit()
阻碍主线程。
SharedPreferences的使用和示例
一般而言SharedPreferences
的名字和主键名都是固定的,所以可以定义一些final
的字符串变量来保存这些名字,在读取和写回时都使用这些常熟变量。对于之前展示的XML对应的代码就如下边所示:
[codesyntax lang=”java” lines=”normal”]
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); }
[/codesyntax]
既然SharedPreferences
的名字是可以任意给定的,所以对于SharedPreferences
的使用就可以有非常的针对性创建不同的文件来存储不同的内容。比如可以以不同用户为名存放他们的偏好信息,可以根据不同界面保存布局信息、已访问的页码。Activity
就额外实现了获取SharedPreferences
的方法getPreferences(int)
,它只需要开发者提供文件的打开模式,自动以Activity
的类名作为文件名。
SharedPreferences
取值时是直接将给定主键在Map中的值强制转换成所需要的值,所以如果用putInt存储了整型却用getBoolean()来取,并不会自动转换成布尔型,而是直接抛出异常,所以要使用要注意保持类型一致。
另外如果没有存储过某个主键,SharedPreferences
会返回null
值,而对于String
、Set<String>
这些类型同样可以存储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
绝对是首选对象。
阅读后加深了我对SharedPreference的理解。项目中一直是用 PreferenceManager.getDefaultSharedPreferences(Context context), 来获取SharedPreferences。原来其实还是调用了getSharedPreferences(String, int),只不过系统自动把default package name 和MODE_PRIVATE 作为参数放进去了。
附一下Android source code:
public static SharedPreferences getDefaultSharedPreferences(Context context) {
return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
getDefaultSharedPreferencesMode());
}
private static String getDefaultSharedPreferencesName(Context context) {
return context.getPackageName() + “_preferences”;
}
private static int getDefaultSharedPreferencesMode() {
return Context.MODE_PRIVATE;
}
没错,也可以自己setSharedPreferencesName()来改变名称,设计得挺灵活的。
关于PreferenceManage,我是打算在下一篇写android.preference.Preference的时候写。
开发Android比开发iOS好的地方在于开源,什么不懂的读源码就能知道的更清楚。光看文档有时候总是很不明确,要是文档缺失就更惨了。
同感,从IOS转到Andriod后切实感受到了开源的好处。有时候文档里不是太清楚的描述,看了源码就清楚了。而且花些时间看源码还能提高自己写代码的思路和质量。