一、简介
-
SharedPreferences
是 Android 提供的数据持久化的一种手段,适合单进程、小批量的数据存储与访问。 - 因为
SharedPreferences
的实现是基于单个xml文件实现的,并且,所有持久化数据都是一次性加载到内存,如果数据过大,是不合适采用SharedPreferences
存放的。 - 而适用的场景是单进程的原因同样如此,由于Android原生的文件访问并不支持多进程互斥,所以
SharePreferences
也不支持,如果多个进程更新同一个xml文件,就可能存在同不互斥问题。 -
SharePreferences
对应的xml文件位置一般都在/data/data/包名/shared_prefs
目录下。
二、如何使用
//写入信息
//打开Preferences,名称为setting,如果存在则打开它,否则创建新的Preferences
SharedPreferences setting = getSharedPreferences("hello",MODE_PRIVATE);
//让setting处于编辑状态
SharedPreferences.Editor editor = setting.edit();
//存放数据
editor.putString("name","jacky");
//完成提交
editor.commit();
// editor.apply();
//读取信息
String name = setting.getString("name","0");
三、SPMode值说明
-
MODE_PRIVATE
:文件是私有数据,只能被应用本身访问,在该模式下写入的内容会覆盖原文件的内容。 -
MODE_WORLD_READABLE
:当前文件可以被其他应用读取,但是不可以进行写入。 -
MODE_WORLD_WRITEABLE
:当前文件可以被其他应用写入,ps: 如果需要被其他应用写入和读取可以直接写MODE_WORLD_WRITEABLE + -MODE_WORLD_READABLE。 -
MODE_APPEND
:该模式会检查文件是否存在,存在就往文件追加内容,否则就创建新的文件。
四、SharePreference 线程安全吗?
是不是线程安全主要看 SharePreference
的保存信息的方法,也就是commit
方法和apply
方法。
首先看一下 android.app.SharedPreferencesImpl.EditorImpl#commit
方法的源码:
@Override
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
// synchronized 提交到内存
MemoryCommitResult mcr = commitToMemory();
// synchronized 磁盘写入
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
} finally {
if (DEBUG) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " committed after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
- 对于提交到内存的方法
commitToMemory
和磁盘写入的方法enqueueDiskWrite
,都广泛使用了synchronized
关键字来保证其线程安全。 - 而且,
commit
函数是在当前线程直接写入文件。 - 最后还使用了阻塞操作,来等待其余的线程操作完毕。
- 所以
commit
操作在多线程下是线程安全的。且注意到使用了try-catch
。
然后看一下 android.app.SharedPreferencesImpl.EditorImpl#apply
方法的源码:
@Override
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
if (DEBUG && mcr.wasWritten) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " applied after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
// Okay to notify the listeners before it's hit disk
// because the listeners should always get the same
// SharedPreferences instance back, which has the
// changes reflected in memory.
notifyListeners(mcr);
}
可以看到:
- 和
commit
一样,也是首先调了commitToMemory
这一步。 - 但是下一步就有区别了,
apply
函数通过子线程完成的写文件的操作。
综上所述
-
commit()
是线程安全的,但是性能慢,同步操作,在当前线程完成写文件操作。 -
apply()
是线程不安全的,但是性能高,是异步处理IO操作。
五、SharePreference与多进程
其实在sp创建的时候可以指定的加载模式中有个MODE_MULTI_PROCESS,它是Google提供的一个在多线程模式。但是这种模式并不是我们说的支持多进程同步更新等,它的作用只会在getSharedPreferences的时候,才会重新从xml重加载,如果我们在一个进程中更新xml,但是没有通知另一个进程,那么另一个进程的SharePreferences是不会自动更新的。
/**
* 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
* targeting such releases. For applications targeting 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}.
*/
@Deprecated
public static final int MODE_MULTI_PROCESS = 0x0004;
官方也建议使用 ContentProvider
来替代 SharePreference
在多线程中的作用。
六、SharePreference原理解析
我们从 android.app.ContextImpl#getSharedPreferences(java.lang.String, int)
函数开始看:
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
// 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";
}
}
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
file = mSharedPrefsPaths.get(name);
if (file == null) {
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
checkMode(mode);
if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
if (isCredentialProtectedStorage()
&& !getSystemService(UserManager.class)
.isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
throw new IllegalStateException("SharedPreferences in credential encrypted "
+ "storage are not available until after user is unlocked");
}
}
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, 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;
}
解读起来也不复杂,先去内存中查询与xml
对应的SharePreferences
是否已经被创建加载,如果没有那么该创建就创建,该加载就加载。在加载之后,要将所有的key-value
保存起来,当然,如果首次访问,可能连xml
文件都不存在,那么还需要创建xml
文件,与SharePreferences
对应的xml
文件位置一般都在/data/data/包名/shared_prefs
目录下,后缀一定是.xml
,数据存储样式如下:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="login_department"></string>
<int name="user_im" value="1" />
<string name="mobile">185xxxxxxxx</string>
<boolean name="HAS_SHOW_BIOMETRIC_DIALOG" value="true" />
<string name="login_userid"></string>
<string name="login_ap_username"></string>
<boolean name="isbackground" value="true" />
<string name="cbb_app_version">2.19.0</string>
<string name="appRun">run</string>
<string name="update_session_key_time">2022-07-15</string>
<string name="device_info_statistics_time">2022-07-15</string>
<boolean name="setting_reversal" value="true" />
<string name="login_face_url"></string>
<string name="login_role"></string>
<string name="session_key"></string>
<string name="app_first_start_time"></string>
<string name="department_two">行政</string>
<boolean name="setting_bright" value="true" />
<string name="login_realname"></string>
<boolean name="recovery_audio_is_finish" value="true" />
<string name="updata_im_contant_time">2022-07-15</string>
</map>
那么,创建好对象的 xml
文件后,结合之前说的 commit
方法和apply
方法,流程就连贯起来了。
七、总结
SharePreferences
是Android基于xml实现的一种数据持久化手段。SharePreferences
不适合存储过大的数据, 因为所有持久化数据都是一次性加载到内存, 数据过大容易造成内存溢出。SharePreferences
的commit
与apply
一个是同步一个是异步(大部分场景下),SharePreferences
的commit
方法是直接在当前线程执行文件写入操作, 而apply
方法是在工作线程执行文件写入, 尽可能使用apply
, 因为不会阻塞当前线程。SharePreferences
并不支持跨进程, 因为它不能保证更新本地数据后被另一个进程所知道,而且跨进程的操作标记已经被弃用。SharePreferences
批量更改数据时,只需要保留最后一个apply即可,避免添加多余的写文件任务。每个
SharePreferences
存储的键值对不宜过多, 否则在加载文件数据到内存时会耗时过长, 而阻塞SharePreferences
的相关get或put方法, 造成ui卡顿。频繁更改的配置项和不常更改的配置项应该分开为不同的
SharePreferences
存放,避免不必要的io操作。commit
有相应的返回值,可以知道操作是否成功,apply
没有返回值。
最后附上参考的文章:
关于SharePreference使用以及内部原理简单解析
SharePreference原理
SharedPreferences线程安全吗?commit和apply的区别?