Android SharedPreference详解
SharedPreferences作为一种数据持久化的方式,是处理简单的key-value类型数据时的首选。
一般用法:
//demo是该sharedpreference对应文件名,对应的是一个xml文件,里面存放key-value格式的数据.
SharedPreferences sharedPreferences = context.getSharedPreferences("demo", MODE_WORLD_WRITEABLE);
//提供了getXXX的读取数据方法
boolean xxx = sharedPreferences.getBoolean("xxx", false);
//通过Editor提供了putXXX系列的存储方法,调用完需要使用apply()或commit()使之生效,不同点后面介绍
SharedPreferences.Editor edit = sharedPreferences.edit();
edit.putBoolean("xxx", true);
edit.apply();//使存储生效
//edit.commit();//使存储生效
每个SharedPreferences都对应了当前package的data/data/package_name/share_prefs/
目录下的一个文件
源码解析
Context.java中getSharedPreferences接口说明:
/*** Retrieve and hold the contents of the preferences file 'name', returning* a SharedPreferences through which you can retrieve and modify its* values. Only one instance of the SharedPreferences object is returned* to any callers for the same name, meaning they will see each other's* edits as soon as they are made.** @param name Desired preferences file. If a preferences file by this name* does not exist, it will be created when you retrieve an* editor (SharedPreferences.edit()) and then commit changes (Editor.commit()).* @param mode Operating mode. Use 0 or {@link #MODE_PRIVATE} for the* default operation, {@link #MODE_WORLD_READABLE}* and {@link #MODE_WORLD_WRITEABLE} to control permissions.** @return The single {@link SharedPreferences} instance that can be used* to retrieve and modify the preference values.** @see #MODE_PRIVATE* @see #MODE_WORLD_READABLE* @see #MODE_WORLD_WRITEABLE*/public abstract SharedPreferences getSharedPreferences(String name,int mode);
ContextImpl中getSharedPreferences实现:
@Overridepublic SharedPreferences getSharedPreferences(String name, int mode) {SharedPreferencesImpl sp;synchronized (ContextImpl.class) {if (sSharedPrefs == null) {sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();}final String packageName = getPackageName();ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);if (packagePrefs == null) {packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();sSharedPrefs.put(packageName, packagePrefs);}// At least one application in the world actually passes in a null// name. This happened to work because when we generated the file name// we would stringify it to "null.xml". Nice.if (mPackageInfo.getApplicationInfo().targetSdkVersion <Build.VERSION_CODES.KITKAT) {if (name == null) {name = "null";}}sp = packagePrefs.get(name);if (sp == null) {File prefsFile = getSharedPrefsFile(name);sp = new SharedPreferencesImpl(prefsFile, mode);packagePrefs.put(name, sp);return sp;}}if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {// If somebody else (some other process) changed the prefs// file behind our back, we reload it. This has been the// historical (if undocumented) behavior.sp.startReloadIfChangedUnexpectedly();}return sp;}
这段代码里,我们可以看出,
- SharedPreferencesImpl是保存在全局个map cache里的,只会创建一次。
- MODE_MULTI_PROCESS模式下,每次获取都会尝试去读取文件reload。当然会有一些逻辑尽量减少读取次数,比如当前是否有正在进行的读取操作,文件的修改时间和大小与上次有没有变化等。
Context.java中提供了以下四种mode:
//这是默认模式,仅caller uid的进程可访问
/*** File creation mode: the default mode, where the created file can only* be accessed by the calling application (or all applications sharing the* same user ID).* @see #MODE_WORLD_READABLE* @see #MODE_WORLD_WRITEABLE*/
int MODE_PRIVATE = 0x0000;//所有人可写,也就是任何应用都可修改它,这是极其危险的,因此改选项已被Deprected
/*** @deprecated Creating world-readable files is very dangerous, and likely* to cause security holes in applications. It is strongly discouraged;* instead, applications should use more formal mechanism for interactions* such as {@link ContentProvider}, {@link BroadcastReceiver}, and* {@link android.app.Service}. There are no guarantees that this* access mode will remain on a file, such as when it goes through a* backup and restore.* File creation mode: allow all other applications to have read access* to the created file.* @see #MODE_PRIVATE* @see #MODE_WORLD_WRITEABLE*/
int MODE_WORLD_READABLE = 0x0001; //所有人可读,这个参数同样非常危险,可能导致隐私数据泄漏
/*** @deprecated Creating world-writable files is very dangerous, and likely* to cause security holes in applications. It is strongly discouraged;* instead, applications should use more formal mechanism for interactions* such as {@link ContentProvider}, {@link BroadcastReceiver}, and* {@link android.app.Service}. There are no guarantees that this* access mode will remain on a file, such as when it goes through a* backup and restore.* File creation mode: allow all other applications to have write access* to the created file.* @see #MODE_PRIVATE* @see #MODE_WORLD_READABLE*/
int MODE_WORLD_READABLE = 0x0002//设置该参数后,每次获取对应的SharedPreferences时都会尝试从磁盘中读取修改过的文件
/*** SharedPreference loading flag: when set, the file on disk will* be checked for modification even if the shared preferences* instance is already loaded in this process. This behavior is* sometimes desired in cases where the application has multiple* processes, all writing to the same SharedPreferences file.* Generally there are better forms of communication between* processes, though.** <p>This was the legacy (but undocumented) behavior in and* before Gingerbread (Android 2.3) and this flag is implied when* targetting such releases. For applications targetting SDK* versions <em>greater than</em> Android 2.3, this flag must be* explicitly set if desired.** @see #getSharedPreferences** @deprecated MODE_MULTI_PROCESS does not work reliably in* some versions of Android, and furthermore does not provide any* mechanism for reconciling concurrent modifications across* processes. Applications should not attempt to use it. Instead,* they should use an explicit cross-process data management* approach such as {@link android.content.ContentProvider ContentProvider}.*/
int MODE_MULTI_PROCESS = 0x0004;
MODE_MULTI_PROCESS
当设置MODE_MULTI_PROCESS这个参数的时候,即使当前进程内已经创建了该SharedPreferences,仍然在每次获取的时候都会尝试从本地文件中刷新。在同一个进程中,同一个文件只有一个实例。MODE_MULTI_PROCESS的作用如上getSharedPreferences实现.这个方法先判断是否已创建SharedPreferences实例,若未创建,则先创建。之后判断mode如果为MODE_MULTI_PROCESS, 则调用startReloadIfChangeUnexpectedly(),看下其实现:
SharedPreferencesImpl.java
void startReloadIfChangedUnexpectedly() {synchronized (this) {// TODO: wait for any pending writes to disk?if (!hasFileChangedUnexpectedly()) {return;}startLoadFromDisk();}}private void startLoadFromDisk() {synchronized (this) {mLoaded = false;}new Thread("SharedPreferencesImpl-load") {public void run() {synchronized (SharedPreferencesImpl.this) {loadFromDiskLocked();}}}.start();}
可以看出MODE_MULTI_PROCESS的作用就是在每次获取SharedPreferences实例的时候尝试从磁盘中加载修改过的数据,并且读取是在异步线程中,因此一个线程的修改最终会反映到另一个线程,但不能立即反映到另一个进程,所以通过SharedPreferences无法实现多进程同步。
综合: 如果仅仅让多进程可访问同一个SharedPref文件,不需要设置MODE_MULTI_PROCESS, 如果需要实现多进程同步,必须设置这个参数,但也只能实现最终一致,无法即时同步。
由于SharedPreference内容都会在内存里存一份,所以不要使用SharedPreference保存较大的内容,避免不必要的内存浪费。
注意有一个锁mLoaded ,在对SharedPreference做其他操作时,都必须等待该锁释放:
@Nullablepublic String getString(String key, @Nullable String defValue) {synchronized (this) {awaitLoadedLocked();String v = (String)mMap.get(key);return v != null ? v : defValue;}}
写操作有两个commit apply 。 commit 是同步的,写入内存的同时会等待写入文件完成,apply是异步的,先写入内存,在异步线程里再写入文件。apply肯定要快一些,优先推荐使用apply:
/*** Commit your preferences changes back from this Editor to the* {@link SharedPreferences} object it is editing. This atomically* performs the requested modifications, replacing whatever is currently* in the SharedPreferences.** <p>Note that when two editors are modifying preferences at the same* time, the last one to call commit wins.** <p>If you don't care about the return value and you're* using this from your application's main thread, consider* using {@link #apply} instead.** @return Returns true if the new values were successfully written* to persistent storage.*/boolean commit();/*** Commit your preferences changes back from this Editor to the* {@link SharedPreferences} object it is editing. This atomically* performs the requested modifications, replacing whatever is currently* in the SharedPreferences.** <p>Note that when two editors are modifying preferences at the same* time, the last one to call apply wins.** <p>Unlike {@link #commit}, which writes its preferences out* to persistent storage synchronously, {@link #apply}* commits its changes to the in-memory* {@link SharedPreferences} immediately but starts an* asynchronous commit to disk and you won't be notified of* any failures. If another editor on this* {@link SharedPreferences} does a regular {@link #commit}* while a {@link #apply} is still outstanding, the* {@link #commit} will block until all async commits are* completed as well as the commit itself.** <p>As {@link SharedPreferences} instances are singletons within* a process, it's safe to replace any instance of {@link #commit} with* {@link #apply} if you were already ignoring the return value.** <p>You don't need to worry about Android component* lifecycles and their interaction with <code>apply()</code>* writing to disk. The framework makes sure in-flight disk* writes from <code>apply()</code> complete before switching* states.** <p class='note'>The SharedPreferences.Editor interface* isn't expected to be implemented directly. However, if you* previously did implement it and are now getting errors* about missing <code>apply()</code>, you can simply call* {@link #commit} from <code>apply()</code>.*/void apply();
注册/解注册sharedpreference变动监听:
/*** Registers a callback to be invoked when a change happens to a preference.** <p class="caution"><strong>Caution:</strong> The preference manager does* not currently store a strong reference to the listener. You must store a* strong reference to the listener, or it will be susceptible to garbage* collection. We recommend you keep a reference to the listener in the* instance data of an object that will exist as long as you need the* listener.</p>** @param listener The callback that will run.* @see #unregisterOnSharedPreferenceChangeListener*/void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);/*** Unregisters a previous callback.* * @param listener The callback that should be unregistered.* @see #registerOnSharedPreferenceChangeListener*/void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
为什么不推荐使用MODE_MULTI_PROCESS?
android文档已经Deprected了这个flag,并且说明不应该通过SharedPreference做进程间数据共享?这是为啥呢?从前面但分析可看到当设置这个flag后,每次获取(获取而不是初次创建)SharedPreferences实例的时候,会判断shared_pref文件是否修改过:
private boolean hasFileChangedUnexpectedly() {synchronized (this) {if (mDiskWritesInFlight > 0) {// If we know we caused it, it's not unexpected.if (DEBUG) Log.d(TAG, "disk write in flight, not unexpected.");return false;}}final StructStat stat;try {/** Metadata operations don't usually count as a block guard* violation, but we explicitly want this one.*/BlockGuard.getThreadPolicy().onReadFromDisk();stat = Os.stat(mFile.getPath());} catch (ErrnoException e) {return true;}synchronized (this) {return mStatTimestamp != stat.st_mtime || mStatSize != stat.st_size;}}
这里先判断mDiskWritesInFlight>0,如果成立,说明是当前进程修改了文件,不需要重新读取。然后通过文件最后修改时间,判断文件是否修改过。如果修改了,则重新读取:
private void startLoadFromDisk() {synchronized (this) {mLoaded = false;}new Thread("SharedPreferencesImpl-load") {public void run() {synchronized (SharedPreferencesImpl.this) {loadFromDiskLocked();}}}.start();
}private void loadFromDiskLocked() {if (mLoaded) {return;}if (mBackupFile.exists()) {mFile.delete();mBackupFile.renameTo(mFile);}Map map = null;StructStat stat = null;try {stat = Os.stat(mFile.getPath());if (mFile.canRead()) {BufferedInputStream str = null;try {str = new BufferedInputStream(new FileInputStream(mFile), 16*1024);map = XmlUtils.readMapXml(str);} finally {IoUtils.closeQuietly(str);}}} catch (ErrnoException e) {}mLoaded = true;if (map != null) {mMap = map;mStatTimestamp = stat.st_mtime;mStatSize = stat.st_size;} else {mMap = new HashMap<String, Object>();}notifyAll();
}
这里起码有3个坑!
- 使用MODE_MULTI_PROCESS时,不要保存SharedPreference变量,必须每次都从context.getSharedPreferences 获取。如果你图方便使用变量存了下来,那么无法触发reload,有可能两个进程数据不同步。
- 前面提到过,load数据是耗时的,并且其他操作会等待该锁。这意味着很多时候获取SharedPreference数据都不得不从文件再读一遍,大大降低了内存缓存的作用。文件读写耗时也影响了性能。
- 修改数据时得用commit,保证修改时写入了文件,这样其他进程才能通过文件大小或修改时间感知到。
重点是这段:
if (mBackupFile.exists()) {mFile.delete();mBackupFile.renameTo(mFile);
}
重新读取时,如果发现存在mBackupFile,则将原文件mFile删除,并将mBackupFile重命名为mFile。mBackupFile又是如何创建的呢?答案是在修改SharedPreferences时将内存中的数据写会磁盘时创建的:
private void writeToFile(MemoryCommitResult mcr) {// Rename the current file so it may be used as a backup during the next readif (mFile.exists()) {if (!mBackupFile.exists()) {if (!mFile.renameTo(mBackupFile)) {mcr.setDiskWriteResult(false);return;}} else {mFile.delete();}}FileOutputStream str = createFileOutputStream(mFile);XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);FileUtils.sync(str);str.close();ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);final StructStat stat = Os.stat(mFile.getPath());synchronized (this) {mStatTimestamp = stat.st_mtime;mStatSize = stat.st_size;}// Writing was successful, delete the backup file if there is one.mBackupFile.delete();mcr.setDiskWriteResult(true);return;}
这段代码只保留了核心流程,忽略了错误处理流程。可以看到,写文件的步骤大致是:
- 将原文件重命名为mBackupFile
- 重新创建原文件mFile, 并将内容写入其中
- 删除mBackupFile
所以,只有当一个进程正处于写文件的过程中的时候,如果另一个进程读文件,才会看到mBackupFile, 这时候读进程会将mBackupFile重命名为mFile, 这样读结果是,读进程只能读到修改前的文件,同时,由于mBackupFile重命名为了mFile, 所以写进程写那个文件就没有文件名引用了,因此其写入的内容无法再被任何进程访问到。所以其内容丢失了,可认为写入失败了,而SharedPreferences对这种失败情况没有任何重试机制,所以就可能出现数据丢失的情况。
回到这段的重点:为什么不推荐用MODE_MULTI_PROCESS?从前面分析可知,这种模式下,每次获取SharedPreferences都会检测文件是否改变,只要读的时候另一进程在写,就会导致写丢失。这样失败概率就会大幅度提高。反之,若不设置这个模式,则只在第一次创建SharedPreferences的时候读取,导致写失败的概率就会大幅度降低,当然,仍然存在失败的可能。
为什么不做写失败重试?
为什么android不做写失败重试呢?原因是写进程并不能发现写失败的情况。难道写的过程中,目标文件被删不会抛异常吗?答案是不会。删除文件只是从文件系统中删除了一个节点信息而已,重命名也是新建了一个具有相同名称的节点信息,并把文件地址指向另一个磁盘地址而已,原来,之前的写过程仍然会成功写到原来的磁盘地址。所以目前的实现方案并不能检测到失败。
有没有办法解决写失败呢?
个人觉得是可以做到的,读里面读那段关键操作:
if (mBackupFile.exists()) {mFile.delete();mBackupFile.renameTo(mFile);
}
mBackupFile存在,意味着当前正处于写读过程中,这时候是不是可以考虑直接读mBackupFile文件,而不删除mFile呢?这样读话,读取效果一样,都是读的mBackupFile,同时写进程写的mFile也不会被mBacupFile覆盖,写也就能成功了。即使通过这段代码重命名,写进程写完后发现mBackupFile不存在了,其实也能认为发生了读重命名,大可以重试一次。
多进程使用SharedPreference方案
说简单也简单,就是依据google的建议使用ContentProvider了。我看过网上很多的例子,但总是觉得少了点什么
有的方案里将所有读取操作都写作静态方法,没有继承SharedPreference 。 这样做需要强制改变调用者的使用习惯,不怎么好。
大部分方案做成ContentProvider后,所有的调用都走的ContentProvider。但如果调用进程与SharedPreference 本身就是同一个进程,只用走原生的流程就行了,不用拐个弯去访问ContentProvider,减少不必要的性能损耗。
我这里也写了一个跨进程方案,简单介绍如下
SharedPreferenceProxy 继承SharedPreferences。其所有操作都是通过ContentProvider完成。简要代码:
public class SharedPreferenceProxy implements SharedPreferences {
@Nullable@Overridepublic String getString(String key, @Nullable String defValue) {OpEntry result = getResult(OpEntry.obtainGetOperation(key).setStringValue(defValue));return result == null ? defValue : result.getStringValue(defValue);}@Overridepublic Editor edit() {return new EditorImpl();}private OpEntry getResult(@NonNull OpEntry input) {try {Bundle res = ctx.getContentResolver().call(PreferenceUtil.URI, PreferenceUtil.METHOD_QUERY_VALUE, preferName, input.getBundle());return new OpEntry(res);} catch (Exception e) {e.printStackTrace();return null;}
...public class EditorImpl implements Editor {private ArrayList<OpEntry> mModified = new ArrayList<>();@Overridepublic Editor putString(String key, @Nullable String value) {OpEntry entry = OpEntry.obtainPutOperation(key).setStringValue(value);return addOps(entry);}@Overridepublic void apply() {Bundle intput = new Bundle();intput.putParcelableArrayList(PreferenceUtil.KEY_VALUES, convertBundleList());intput.putInt(OpEntry.KEY_OP_TYPE, OpEntry.OP_TYPE_APPLY);try {ctx.getContentResolver().call(PreferenceUtil.URI, PreferenceUtil.METHOD_EIDIT_VALUE, preferName, intput);} catch (Exception e) {e.printStackTrace();}
...}
...}
OpEntry只是一个对Bundle操作封装的类。
所有跨进程的操作都是通过SharedPreferenceProvider的call方法完成。SharedPreferenceProvider里会访问真正的SharedPreference
public class SharedPreferenceProvider extends ContentProvider{private Map<String, MethodProcess> processerMap = new ArrayMap<>();@Overridepublic boolean onCreate() {processerMap.put(PreferenceUtil.METHOD_QUERY_VALUE, methodQueryValues);processerMap.put(PreferenceUtil.METHOD_CONTAIN_KEY, methodContainKey);processerMap.put(PreferenceUtil.METHOD_EIDIT_VALUE, methodEditor);processerMap.put(PreferenceUtil.METHOD_QUERY_PID, methodQueryPid);return true;}@Nullable@Overridepublic Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) {MethodProcess processer = processerMap.get(method);return processer == null?null:processer.process(arg, extras);}
...
}
重要差别的地方在这里:在调用getSharedPreferences时,会先判断caller的进程pid是否与SharedPreferenceProvider相同。如果不同,则返回SharedPreferenceProxy。如果相同,则返回ctx.getSharedPreferences。只会在第一次调用时进行判断,结果会保存起来。
public static SharedPreferences getSharedPreferences(@NonNull Context ctx, String preferName) {//First check if the same processif (processFlag.get() == 0) {Bundle bundle = ctx.getContentResolver().call(PreferenceUtil.URI, PreferenceUtil.METHOD_QUERY_PID, "", null);int pid = 0;if (bundle != null) {pid = bundle.getInt(PreferenceUtil.KEY_VALUES);}//Can not get the pid, something wrong!if (pid == 0) {return getFromLocalProcess(ctx, preferName);}processFlag.set(Process.myPid() == pid ? 1 : -1);return getSharedPreferences(ctx, preferName);} else if (processFlag.get() > 0) {return getFromLocalProcess(ctx, preferName);} else {return getFromRemoteProcess(ctx, preferName);}}private static SharedPreferences getFromRemoteProcess(@NonNull Context ctx, String preferName) {synchronized (SharedPreferenceProxy.class) {if (sharedPreferenceProxyMap == null) {sharedPreferenceProxyMap = new ArrayMap<>();}SharedPreferenceProxy preferenceProxy = sharedPreferenceProxyMap.get(preferName);if (preferenceProxy == null) {preferenceProxy = new SharedPreferenceProxy(ctx.getApplicationContext(), preferName);sharedPreferenceProxyMap.put(preferName, preferenceProxy);}return preferenceProxy;}}private static SharedPreferences getFromLocalProcess(@NonNull Context ctx, String preferName) {return ctx.getSharedPreferences(preferName, Context.MODE_PRIVATE);}
这样,只有当调用者是正真跨进程时才走的contentProvider。对于同进程的情况,就没有必要走contentProvider了。对调用者来说,这都是透明的,只需要获取SharedPreferences就行了,不用关心获得的是SharedPreferenceProxy,还是SharedPreferenceImpl。即使你当前没有涉及到多进程使用,将所有获取SharedPreference的地方封装并替换后,对当前逻辑也没有任何影响。