一、简介
SharedPreferences是一种轻量级的数据存储方式,采用键值对的存储方式。
SharedPreferences只能存储少量数据,大量数据不能使用该方式存储,支持存储的数据类型有boolean, float, int, long, and string。
SharedPreferences存储到一个XML文件中的,路径在/data/data/$packagename/shared_prefs/下。该文件夹不root时无法通过文件系统访问,但可以通过如下步骤进入到这个文件夹。
adb devices 获取设备列表
adb -s <device name> shell 进入设备根目录
如果只连接了一个设备,可以直接使用 adb shell 进入根目录
run-as <package name> 进入应用目录,注意必须是debug包,本质上是打包时保证"debuggable true"。
ls 查看文件列表
cd shared_prefs
ls 查看文件列表
cat <package name> <filename>.xml 查看对应文件的内容
一个SP文件的示例
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<long name="key_settings_time" value="0" />
<string name="key_ctx_info_Bullet">eyJlI</string>
</map>
二、用法示例
/**
* 向Preference中存储一条数据
*/
private fun saveValue(key: String, value: String) {
// 1.获取SharedPreferences实例,"data"表示SP文件名
val sharedPreferences = getSharedPreferences("data", Context.MODE_PRIVATE)
// 2.调用SharedPreferences对象的edit()方法来获取一个SharedPreferences.Editor对象
val editor = sharedPreferences.edit()
// 3.向eidtor中存放数据
editor.putString(key, value)
// 4.将数据同步到内存和文件中
editor.apply()
}
/**
* 从SharedPreferences中获取数据
*/
private fun getValue(key: String) {
// 1.获取SharedPreferences实例
val sharedPreferences = getSharedPreferences("data", Context.MODE_PRIVATE)
// 2.读取数据
val value = sharedPreferences.getString(key, "default")
}
三、获取SharedPreferences实例的方法
方法1:Context.getSharedPreferences(String name, int mode)
-
name:文件名,文件不存在的时候将会自动创建该文件
-
mode:操作模式,创建文件时对文件添加可操作的模式
-
// Context.java // XML文件只允许创建它的应用访问,或者是拥有相同userId的应用访问(同应用多进程) public static final int MODE_PRIVATE = 0x0000; // 已废弃,允许所有其他应用读取该文件 @Deprecated public static final int MODE_WORLD_READABLE = 0x0001; // 已废弃,允许所有其他应用写入该文件 @Deprecated public static final int MODE_WORLD_WRITEABLE = 0x0002 // 已废弃,允许其他所有应用访问该文件 @Deprecated public static final int MODE_MULTI_PROCESS = 0x0004;也就是说mode现在只能填MODE_PRIVATE了
方法2:Activity.getPreferences(int mode)
// Activity.java
/**
* 调用方法1,只是以当前Activity的类名作为文件名了
*/
public SharedPreferences getPreferences(int mode) {
return getSharedPreferences(getLocalClassName(), mode);
}
方法3:PreferenceManager.getDefaultSharedPreferences(Context context)。PrefereceManager已废弃
// PreferenceManager.java
/**
* 调用方法1,以当前应用的包名+"_preferences"作为文件名
*/
public static SharedPreferences getDefaultSharedPreferences(Context context) {
return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
getDefaultSharedPreferencesMode());
}
public static String getDefaultSharedPreferencesName(Context context) {
return context.getPackageName() + "_preferences";
}
private static int getDefaultSharedPreferencesMode() {
return Context.MODE_PRIVATE;
}
四、源码解析
1.Context.getSharedPreferences(String name, int mode)获取SharedPreferences流程
抽象方法,其实现类是ContextImpl。关于Context与ContextImpl可以参考这篇文章
// ContextImpl.java
// SP文件与其对应的SharedPreferencesImpl的集合,static修饰,全局只有一个。
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
// 4.4之前,name = null时默认使用 "null"作为文件名
if (mPackageInfo.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
if (name == null) {
name = "null";
}
}
File file;
synchronized (ContextImpl.class) {
// 使用ArrayMap()来保存name与其对应的文件
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
file = mSharedPrefsPaths.get(name);
if (file == null) {
// 在特定目录下创建文件
file = getSharedPreferencesPath(name);
// 将name与file的对应关系放到ArrayMap()中
mSharedPrefsPaths.put(name, file);
}
}
// 通过文件对象去取sp
// mSharedPrefsPaths不是单例的,每个Context有一份,他们拿到的file对象不一致
// 但是不同的file却对应着同一个磁盘文件,它们的HashCode是一致的,因此都能获取到唯一对应的SharedPreferencesImpl对象
// File的hashCode计算参考 UnixFileSystem.hashCode(file)方法 f.getPath().hashCode() ^ 1234321;
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");
}
}
// 将file与对应的sp对象放入到cache(ArrayMap中)
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;
}
@GuardedBy("ContextImpl.class")
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
// sSharedPrefsCache是静态的,进程中只有一个
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>();
}
// sSharedPrefsCache中保存着需要的ArrayMap()
final String packageName = getPackageName();
ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}
return packagePrefs;
}
2.SharedPreferences解析数据流程
// SharedPreferencesImpl.java
SharedPreferencesImpl(File file, int mode) {
...
startLoadFromDisk();
}
@UnsupportedAppUsage
private void startLoadFromDisk() {
// 构造方法会加载文件,并阻塞SharedPreferencesImpl。如果获得SharedPreferencesImpl后立即调用get()方法,将会阻塞调用get()的线程
synchronized (mLock) {
mLoaded = false;
}
// 在子线程中加载
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
private void loadFromDisk() {
...
Map<String, Object> map = null;
StructStat stat = null;
Throwable thrown = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
// 读取文件字节流
str = new BufferedInputStream(new FileInputStream(mFile), 16 * 1024);
// 使用XmlUtils将数据,解析成HashMap()
map = (Map<String, Object>) XmlUtils.readMapXml(str);
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}
}
...
mMap = map;
...
}
3.SharedPreferences获取数据流程
// SharedPreferencesImpl.java
public String getString(String key, @Nullable String defValue) {
// 所有的get方法都是同步方法,这个获取数据的代价相对较高了
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
/**
* 由此可以看出,当调用getString()方法时,如果子线程还没有将文件解析完毕,这里将会阻塞
*/
@GuardedBy("mLock")
private void awaitLoadedLocked() {
if (!mLoaded) {
// Raise an explicit StrictMode onReadFromDisk for this
// thread, since the real read will be in a different
// thread and otherwise ignored by StrictMode.
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
4.SharedPreferences.Editor的put()流程
// SharedPreferencesImpl.java
// SharedPreferencesImpl$EditorImpl
@GuardedBy("mEditorLock")
private final Map<String, Object> mModified = new HashMap<>();
@Override
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
// 数据暂存到mModified中
mModified.put(key, value);
return this;
}
}
5.SharedPreferences.Editor的apply()
// SharedPreferencesImpl.java
// SharedPreferencesImpl$EditorImpl
@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) {
}
}
};
// 将计数器加入QueuedWork
QueuedWork.addFinisher(awaitCommit);
// 写入完成之后,应该调用本方法
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
6. SharedPreferences.Editor的commit()
// SharedPreferencesImpl.java
// SharedPreferencesImpl$EditorImpl
@Override
public boolean commit() {
...
// 先写入内存
MemoryCommitResult mcr = commitToMemory();
// 提交到写入队列
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
7.SharedPreferences.Editor的commitToMemory()
// 将本次的改动先同步到内存当中,并返回一个MemoryCommitResult对象
private MemoryCommitResult commitToMemory() {
...
synchronized (SharedPreferencesImpl.this.mLock) {
...
// 将之前已经有的数据取出来
mapToWriteToDisk = mMap;
// 需要向磁盘中写入的任务数+1
mDiskWritesInFlight++;
...
synchronized (mEditorLock) {
boolean changesMade = false;
// 如果是清除操作
if (mClear) {
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
mapToWriteToDisk.clear();
}
keysCleared = true;
mClear = false;
}
// 将mModified中的数据同步进来
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
// "this" is the magic value for a removal mutation. In addition,
// setting a value to "null" for a given key is specified to be
// equivalent to calling remove on that key.
if (v == this || v == null) {
if (!mapToWriteToDisk.containsKey(k)) {
continue;
}
mapToWriteToDisk.remove(k);
} else {
if (mapToWriteToDisk.containsKey(k)) {
Object existingValue = mapToWriteToDisk.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mapToWriteToDisk.put(k, v);
}
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
listeners, mapToWriteToDisk);
}
8.SharedPreferences.Editor的enqueueDiskWrite()
根据apply()和commit()可以看出,两者的postWriteRunnable不同,commit()的postWriteRunnable为null。enqueueDiskWrite方法会根据postWriteRunnable判断出是否是同步提交。
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
// 是否同步写入提交。commit()为true,apply()为false
final boolean isFromSyncCommit = (postWriteRunnable == null);
// 写入磁盘
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
// 写入磁盘
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
// 写入成功,需要写入的任务-1
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
// 写入成功,回调postWriteRunnable
postWriteRunnable.run();
}
}
};
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
// 如果此时只有一个需要commit()的,直接调用writeToDiskRunnable.run()方法
writeToDiskRunnable.run();
return;
}
}
// apply()放入QueuedWork中,在子线程执行
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
9.SharedPreferences.Editor的writeToFile()
/**
* 将保存在内从中的mcr写入磁盘
*/
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
...
try {
FileOutputStream str = createFileOutputStream(mFile);
if (str == null) {
mcr.setDiskWriteResult(false, false);
return;
}
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
writeTime = System.currentTimeMillis();
FileUtils.sync(str);
fsyncTime = System.currentTimeMillis();
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
...
mDiskStateGeneration = mcr.memoryStateGeneration;
// 设置磁盘写入结果
mcr.setDiskWriteResult(true, true);
return;
}
...
mcr.setDiskWriteResult(false, false);
}
10.SharedPreferences$MemoryCommitResult的setDiskWriteResult()
内存当中的数据被写入到磁盘之后将会调用这个方法
final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
void setDiskWriteResult(boolean wasWritten, boolean result) {
this.wasWritten = wasWritten;
writeToDiskResult = result;
// 将writtenToDiskLatch的值归0
writtenToDiskLatch.countDown();
}
注意到apply()和commit()中都会有以下操作。
mcr.writtenToDiskLatch.await();
当执行到这行代码时,当前线程会判断writtenToDiskLatch的值,该值为0时,才可以继续往下操作,否则线程将会阻塞在这里。详细介绍请参考CountDownLatch这个类的用法。
五、SP的缺陷
1.由于是对文件 IO 读取,因此在 IO 上的瓶颈是个大问题
2.多线程场景下效率比较低
因为 get 操作的时候,会锁定 SharedPreferencesImpl 里面的对象,互斥其他操作,而当 put、commit() 和 apply() 操作的时候都会锁住 Editor 的对象,这样的情况下,效率会降低。
3.不建议用于跨进程通讯
SP是文件的一种,只适合正在对数据同步要求不高的进程之间进行并发的读写。而且SP自带内存缓存机制,在多进程模式下,系统对SP文件的控制也将不可靠。
4.加载缓慢,可能会引起卡顿
由于每次都会把整个文件加载到内存中,因此,如果 SharedPreferences 文件过大,或者在其中的键值对是大对象的 json 数据则会占用大量内存,读取较慢是一方面,同时也会引发程序频繁GC,导致的界面卡顿。
六、注意事项
1.不要在单个文件中存入过多的key-value或较大的数据
SharedPreference初始化的时候,会进入到SharedPreferencesImpl构造函数,从磁盘中加载文件资源到内存,加载过程使用了同步锁,而在get和put的操作中同样使用到同步锁,因此,若在SP没初始化完成的情况下,我们的get和put操作将只能等待初始化获得的锁释放。另外,我们看到初始化的时候是从磁盘进行文件读取并且解析xml文件到内存的,因此我们应该尽量保障SP文件不要过大,这样可以尽量保证初始化能快速完成。读取较慢是一方面,同时也会引发程序频繁GC,导致的界面卡顿。可以考虑使用多个SP文件来存储数据,对于实时性比较高的数据,应该单独创建较小的SP文件。
2.频繁修改的数据修改后统一提交,而不是修改过后马上提交
每次修改数据都会创建EditorImpl以及一系列相关对象,每次提交都是一个同步操作,每次提交都是一次I/O操作,因此应该减少提交的次数。
3.在不需要返回值的情况下,使用 apply() 方法可以极大的提高性能
apply()是一个异步方法,它会立即同步到内存当中,但是存储到磁盘的操作会放到子线程中去执行 apply()是在子线程中调用,不会阻塞主线程。但它会持有SP的锁,如果是在apply()之后,立即在主线程中操作SP,那依然是会阻塞的。
4.apply()一定不会阻塞主线程吗
注意到在apply()中有如下操作
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
// 将计数器加入QueuedWork
QueuedWork.addFinisher(awaitCommit);
// 创建向磁盘写入的操作
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
// 当apply()写入文件成功之后会将awaitCommit移除。
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
根据源码分析中,第10个方法的分析,我们知道mcr.writtenToDiskLatch.await()这段代码可能会导致执行它的线程阻塞。写入线程会在执行postWriteRunnable时,触发awaitCommit.run()。但是显然,这个时机是写入线程已经写入完成的时候,因此这里只是个判断标记,并不会真的阻塞。
但是我们注意到,QueuedWork.addFinisher(awaitCommit),同时将这个任务加入了QueuedWork中,如果我们的写入线程没有执行完毕,就不会执行QueuedWork.removeFinisher(awaitCommit)来移除awaitCommit。如果这个时候线程调用了QueuedWork.waitToFinish()方法,它就有可能会被阻塞,直到写入线程将数据全部写入到磁盘。
看下waitToFinish方法的注释
/**
* Trigger queued work to be processed immediately. The queued work is processed on a separate
* thread asynchronous. While doing that run and process all finishers on this thread. The
* finishers can be implemented in a way to check weather the queued work is finished.
*
* Is called from the Activity base class's onPause(), after BroadcastReceiver's onReceive,
* after Service command handling, etc. (so async work is never lost)
*/
public static void waitToFinish() {
}
BroadcastReceiver的onReceive(),Activity的onPause(),Service的onCommond()方法都有可能调用waitToFinish()。如果这个时候,文件尚未写入完毕,就会阻塞,也有ANR的可能性。