在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对象就可以了。 EditorputString(String key, String value); EditorputStringSet(String key, Set<String> values); EditorputInt(String key, int value); EditorputLong(String key, long value); EditorputFloat(String key, float value); EditorputBoolean(String key, boolean value); Editorremove(String key); SharedPreferences 还有一个内嵌接口 OnSharedPreferenceChangeListener,实现它唯一的方法 onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) 并通过以下方法添加在 SharedPreferences对象上就可以监听其上键值对的增加、删除和修改: void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListenerlistener); void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListenerlistener); SharedPreferences的在Android系统中的实现SharedPreferences 和内嵌的 Editor 其实都只是接口定义而已,并没有实现任何方法。它只是用来制定了一个存储键值对的协议,具体的实现方式和存储形式可以是任意的。在Android系统中,它默认以XML格式的文件来存储这些数据,实现的类则是 SharedPreferencesImpl 。 下边就是所保存的XML文件的基本格式,它以数据类型作为XML元素的标签,主键(key)是标签name属性的值,而主键对应的值则作为value属性的值。但如果是String类型则作为标签下的content,这样就不用转义引号也能更好的处理换行。另外对于null 值存储的结构也比较特殊,它以 null 为标签,只有一个 name 属性,没有其他内容。 <?xmlversion='1.0' encoding='utf-8' standalone='yes' ?> <map> <stringname="Name">Ider</string> <booleanname="Android" value="true"/> <setname="Subsites"> <string>code.iderzheng.com</string> <string>blog.iderzheng.com</string> <string>manual.iderzheng.com</string> </set> <intname="VersionCode" value="21"/> <longname="VersionNumber" value="1355"/> <floatname="Version" value="5.0"/> <nullname="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(Contextcontext) { final SharedPreferencesspref = 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.Editoreditor = 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(Contextcontext) { final SharedPreferencesspref = 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 绝对是首选对象。 (责任编辑:好模板) |