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实现:
@Override
public 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做其他操作时,都必须等待该锁释放:
@Nullable
public 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 read
if (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
@Override
public String getString(String key, @Nullable String defValue) {
OpEntry result = getResult(OpEntry.obtainGetOperation(key).setStringValue(defValue));
return result == null ? defValue : result.getStringValue(defValue);
}
@Override
public 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<>();
@Override
public Editor putString(String key, @Nullable String value) {
OpEntry entry = OpEntry.obtainPutOperation(key).setStringValue(value);
return addOps(entry);
}
@Override
public 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<>();
@Override
public 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
@Override
public 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 process
if (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的地方封装并替换后,对当前逻辑也没有任何影响。