Android SharedPreferences
SharedPreferences 是 Android SDK 供给的轻量的数据存储类,用于存储 Key-Value 数据。
运用指南
写入数据
context.getSharedPreferences("name", 0).edit().putString("key", "value").apply()
或是:
context.getSharedPreferences("name", 0).edit().putString("key", "value").commit()
-
apply()
会立即更改内存中的SharedPreferences
目标,但会将更新异步写入磁盘。 -
commit()
将数据同步写入磁盘。但是,因为commit()
是同步的,您应避免从主线程调用它,因为它或许会暂停您的界面出现。
对于这两个办法存在以下一些特性:
- commit 办法会同步的将修正内容同步到磁盘中。并供给了写入成果的回来值。
- apply 办法会将更改先提交到内存中,然后以异步的形式来写入到磁盘中。而且不会提示写入成果。
- 请注意,当两个 Editor 同时修正时,最终一次调用 apply 的会成功。
- 假如先履行了 apply ,在 apply 没有写入磁盘前,调用 commit ,commit 办法会堵塞,直到一切异步都履行完结。
读取数据
context.getSharedPreferences("name", 0).getString("key", defaultValue)
数据文件
SharedPreferences 会将键值对数据写入磁盘中的 XML 文件,文件目录为:
/data/data/应用包名/shared_prefs/文件名.xml
文件名称来自 Context#getSharedPreferences(name, mode)
的第一个参数 name 字符串,其 XML 结构是:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="TestKey">this is sp manager</string>
</map>
原理剖析
Context#getSharedPreferences
运用 SharedPreferences 的办法是经过 Context#getSharedPreferences(name, mode)
来获取一个 SharedPreferences 目标,经过这个目标去读取或是更改数据。经过这个办法会创立一个名称为参数 name 的 XML 文件,name 不同就会创立不同的文件,所以这儿建议大局运用的 SharedPreferences 运用一个大局的 name 。
读数据时,只需求传入两个字符串,name 和 key,下图是一个整体的查找流程:
Context#getSharedPreferences(name, mode)
的源码如下:
public SharedPreferences getSharedPreferences(String name, int mode) {
// KITKAT 前的版别存在 app 传入了一个 null ,将其指向为 null.xml 文件
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);
}
读数据时须确保同步,对 mSharedPrefsPaths 特点进行创立,是一个 ArrayMap ,然后从 mSharedPrefsPaths 中查找到 name 对应的 File ,假如没有 File 会去创立 File 目标,以 name.xml 命名。并将其保存到 mSharedPrefsPaths 中。
// 这儿证明了 XML 文件的命名是根据 name 字符串确认的
public File getSharedPreferencesPath(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
private File makeFilename(File base, String name) {
if (name.indexOf(File.separatorChar) < 0) {
final File res = new File(base, name);
BlockGuard.getVmPolicy().onPathAccess(res.getPath());
return res;
}
throw new IllegalArgumentException(
"File " + name + " contains a path separator");
}
这儿的 mSharedPrefsPaths 是保存在 ContextImpl 中的成员特点,这个目标在 ActivityThread 创立 Application 时,会创立 ContextImpl 作为 Context ,并创立 Application ,所以对于一个应用程序,mSharedPrefsPaths 是单例存在的。
最终调用 getSharedPreferences(File file, int 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;
}
首要先加锁确保读数据时的同步,经过 getSharedPreferencesCacheLocked()
读取 sSharedPrefsCache ,sSharedPrefsCache 缓存的是包名和 SharedPreferencesImpl 的映射联系,这儿从 sSharedPrefsCache 读取到 SharedPreferencesImpl 目标后回来的是保存文件和 SharedPreferencesImpl 的映射联系的 ArrayMap。
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>();
}
final String packageName = getPackageName();
ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}
return packagePrefs;
}
拿到文件和 SharedPreferencesImpl 的映射联系集合后,读取文件对应的 SharedPreferencesImpl 目标,假如没有目标则去创立 SharedPreferencesImpl :
sp = cache.get(file);
// SharedPreferencesImpl 不存在,去创立
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;
}
创立 SharedPreferencesImpl 目标,最重要的是履行 startLoadFromDisk 办法:
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk();
}
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
在这儿发动了一个新线程履行 loadFromDisk()
,后者将会从 XML 文件中读取数据并转化成 Map<String, Object>
类型的目标 :
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) return;
// 清理备份文件
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
Map<String, Object> map = null;
StructStat stat = null;
Throwable thrown = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try { // 读文件,保存到 map
str = new BufferedInputStream(
new FileInputStream(mFile), 16 * 1024);
map = (Map<String, Object>) XmlUtils.readMapXml(str);
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
// An errno exception means the stat failed. Treat as empty/non-existing by
// ignoring.
} catch (Throwable t) {
thrown = t;
}
synchronized (mLock) {
mLoaded = true;
mThrowable = thrown;
// It's important that we always signal waiters, even if we'll make
// them fail with an exception. The try-finally is pretty wide, but
// better safe than sorry.
try {
if (thrown == null) {
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
}
// In case of a thrown exception, we retain the old map. That allows
// any open editors to commit and store updates.
} catch (Throwable t) {
mThrowable = t;
} finally {
mLock.notifyAll();
}
}
}
读取到数据后保存到了 SharedPreferencesImpl 的成员特点 mMap(Map<String, Object>
)中。
Context#getSharedPreferences
最终回来的是一个 SharedPreferencesImpl 目标,这个目标的 mMap 中保存了从 XML 中读取到的数据。
注意:因为
SharedPreference
内容都会在内存里存一份,所以不要运用SharedPreference
保存较大的内容,避免不必要的内存浪费。
Activity#getPreferences
另一种运用办法是在 Activity 中供给了 getPreferences()
办法,这个办法本质上调用的仍是 Context#getSharedPreferences(name, mode)
, 只不过封装了 name 参数的内容:
public SharedPreferences getPreferences(@Context.PreferencesMode int mode) {
return getSharedPreferences(getLocalClassName(), mode);
}
回来的是不含包名的当前类名字符串:
public String getLocalClassName() {
final String pkg = getPackageName();
final String cls = mComponent.getClassName();
int packageLen = pkg.length();
if (!cls.startsWith(pkg) || cls.length() <= packageLen
|| cls.charAt(packageLen) != '.') {
return cls;
}
return cls.substring(packageLen+1);
}
SharedPreferences 读操作
public interface SharedPreferences {
@Nullable
String getString(String key, @Nullable String defValue);
//...
}
SharedPreferences 读取数据的 get 系列办法在 SharedPreferences 接口中直接界说,其完成在 SharedPreferencesImpl 类中:
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
这儿的本质是读取 mMap 特点中的数据,并将其类型强制转化。但在真正读取数据前,会处理同步:
@GuardedBy("mLock")
private void awaitLoadedLocked() {
if (!mLoaded) {
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
这儿是一个自旋等候机制,假如 mLoaded 不为 true 则会进入线程等候状况。 mLoaded 在 loadFromDisk()
办法中读 XML 文件数据时设置成了 true 。
SharedPreferences 写操作
SharedPreferences 接口中对于写操作都界说在了 Editor 接口中:
public interface Editor {
Editor putString(String key, @Nullable String value);
Editor putStringSet(String key, @Nullable Set<String> values);
Editor putInt(String key, int value);
Editor putLong(String key, long value);
Editor putFloat(String key, float value);
Editor putBoolean(String key, boolean value);
Editor remove(String key);
Editor clear();
boolean commit();
void apply();
}
这样的意图是经过封装成链式调用,更加方便地批量操作数据。
Editor 的完成在 SharedPreferencesImpl 中:
public final class EditorImpl implements Editor {
private final Object mEditorLock = new Object();
@GuardedBy("mEditorLock")
private final Map<String, Object> mModified = new HashMap<>();
@GuardedBy("mEditorLock")
private boolean mClear = false;
// ...
}
比较值得注意的是,它内部有一个 HashMap 类型的 mModified 特点,临时保存数据,以 putString 办法为例:
@Override
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
putString 办法会把键值对数据保存的 mModified 中,最终在调用 apply()
或 commit()
时,才将 mModified 中的数据写入到 XML 文件中。
所以,在数据没有写入到磁盘前(在 apply()
或 commit()
没有开端履行写入时),调用 SharedPreferences 的 get 系列办法读数据是读不到的;但 apply()
或 commit()
一旦开端履行写入操作,就会抢占 mLock 锁,这个时候调用 SharedPreferences 的 get 系列办法会被堵塞,等候写入操作完结后释放锁。
commitToMemory
不管是 apply()
仍是 commit()
,在履行写入磁盘前,都需求履行提交到内存的操作, 经过 commitToMemory()
办法进行:
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
boolean keysCleared = false;
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
if (mDiskWritesInFlight > 0) {
mMap = new HashMap<String, Object>(mMap);
}
mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
keysModified = new ArrayList<String>();
listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
synchronized (mEditorLock) {
boolean changesMade = false;
// 调用了 Editor 的 clear 办法,清理数据
if (mClear) {
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
mapToWriteToDisk.clear();
}
keysCleared = true;
mClear = false;
}
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
// 数据不存在或调用了 remove(v == this)
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);
}
在这个办法中,又创立了一个临时的 Map 目标 mapToWriteToDisk ,mapToWriteToDisk 读取了一切的 mMap 并写入了 mModified 中的更新,最终将处理更新后的 mapToWriteToDisk 保存到了 MemoryCommitResult 目标中。
MemoryCommitResult 目标后续会在调用 enqueueDiskWrite(MemoryCommitResult, Runnable)
办法时,将数据写入到磁盘文件中:
private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {
// 是否同步
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--; // 更新计数
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
// 同步履行,需求的额外操作
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run(); // 直接同步履行 runnable ,并回来
return;
}
}
// 异步情况经过 QueuedWork 履行
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
这儿的 writeToFile(mcr, isFromSyncCommit)
履即将数据写入文件的操作。
commit
commit 是同步操作,enqueueDiskWrite(mcr, null)
的第二个参数传了 null ,而且,会发动一个 CountDownLatch 进行等候,履行完结后经过 MemoryCommitResult 的 writeToDiskResult 回来写入成果。
@Override
public boolean commit() {
long startTime = 0;
MemoryCommitResult mcr = commitToMemory();
/* sync write on this thread okay */
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
apply
apply 是异步操作,经过 QueuedWork 履行异步使命,并处理同步。该办法没有回来值。
@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 something
}
}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
// 能够在它抵达磁盘之前通知侦听器,因为侦听器应该一直回来相同的 SharedPreferences 实例,该实例的更改反映在内存中。
notifyListeners(mcr);
}
多进程
尽管 Context.MODE_MULTI_PROCESS
已弃用,但上面的 Context#getSharedPreferences
办法仍有想过逻辑。MODE_MULTI_PROCESS
确保多进程数据正确的办法是每次获取都会尝试去重新 reload 文件,这样在多进程形式下是不可靠的:
- 运用
MODE_MULTI_PROCESS
时,不能保存 SharedPreferences 变量,有必要每次都从context.getSharedPreferences
获取。假如保存变量会无法触发reload,有或许两个进程数据不同步。 - 加载磁盘数据是耗时的,而且其他操作会等候该锁。这意味着许多时候获取 SharedPreferences 数据都不得不从文件再读一遍,大大降低了内存缓存的效果。文件读写耗时也影响了功能。
- 修正数据时得用
commit
,确保修正时写入了文件,这样其他进程才干经过文件大小或修正时刻感知到。
所以在多进程形式下,并不合适运用 SharePreferences,应该选择更好的进程间通讯计划。
一种多线程的 SharePreferences 完成思路是:经过 ContentProvider,当 SharePreferences 调用进程与 ContentProvider 为同一进程时,走 SharePreferences 的完成,当进程不同时,再经过 ContentProvider 完成。
总结
SharedPreferences 需求要点重视的知识包括:
- apply 和 commit 办法的区别。
- Editor 运用链式调用,便于批量操作。
- SharedPreferences 运用了同步锁和线程创立,功能上必定有损耗。
- 多个 Map 直接保存了不同的映射联系:
- mSharedPrefsPaths 保存 name 和 file 的映射联系。
- sSharedPrefsCache 保存 file 和 SharedPreferencesImpl 的映射联系。
- SharedPreferencesImpl 的 mMap 保存 key 和 value 的联系。
- 多个 Map 保存数据:
- mMap 保存从 XML 中加载到内存的数据。
- mModified 保存修正的数据。
- mapToWriteToDisk 兼并了 mMap 和 mModified ,准备写入到磁盘。
- 不适用于多线程。