Android SharedPreferences

SharedPreferences 是 Android 系统提供的一种数据持久化的方式,用于保存简单的数据。

SharedPreferences 是一个接口,实现类是 SharedPreferencesImpl。使用 synchronized 对象锁保证线程安全,非进程安全

源码基于 sdk-30/11.0/R.

获取 SharedPreferences

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// ContextImpl.java
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
if (mPackageInfo.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
if (name == null) {
name = "null";
}
}

File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
// SP 文件名称和 File 映射
mSharedPrefsPaths = new ArrayMap<>();
}
file = mSharedPrefsPaths.get(name);
if (file == null) {
// 在 /data/data/pkgName/shared_prefs 目录下创建 xml 文件
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);
// 创建当前文件的 SP 对象
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}

// 指定 MODE_MULTI_PROCESS 会重新加载 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;
}

// 缓存了所有应用的 SP 对象
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>();
}

final String packageName = getPackageName();
// 获取当前应用所有的 SP 对象
ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}

return packagePrefs;
}
  • 系统缓存了所有应用的 SP 对象。在同一个进程中,同一个文件只有一个 SP 实例,和 xml 文件一一对应;
  • 如果获取 SP 对象时 mode 指定了 Context.MODE_MULTI_PROCESS,会先重新加载 xml 文件,再获取值。

SharedPreferencesImpl 初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// SharedPreferencesImpl.java
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk();
}

void startReloadIfChangedUnexpectedly() {
synchronized (mLock) {
// TODO: wait for any pending writes to disk?
if (!hasFileChangedUnexpectedly()) {
return;
}
startLoadFromDisk();
}
}

private boolean hasFileChangedUnexpectedly() {
synchronized (mLock) {
// 大于 0 表示当前进程正在写入文件
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 {
BlockGuard.getThreadPolicy().onReadFromDisk();
stat = Os.stat(mFile.getPath());
} catch (ErrnoException e) {
return true;
}

// 时间戳或文件大小不一致表示文件有更新
synchronized (mLock) {
return !stat.st_mtim.equals(mStatTimestamp) || mStatSize != stat.st_size;
}
}

private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}

private void loadFromDisk() {
synchronized (mLock) {
// 判断是否需要重新加载
if (mLoaded) {
return;
}
// 备份文件存在,说明写入文件时出现了异常,恢复备份的文件
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}

// Debugging
if (mFile.exists() && !mFile.canRead()) {
Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
}

Map<String, Object> map = null;
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
// 从 xml 文件中读取字符串流
str = new BufferedInputStream(
new FileInputStream(mFile), 16 * 1024);
// 将字符串流转换为 Map 对象
map = (Map<String, Object>) XmlUtils.readMapXml(str);
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}

synchronized (mLock) {
mLoaded = true;
try {
if (thrown == null) {
if (map != null) {
// 赋值全局 Map 对象
mMap = map;
} else {
mMap = new HashMap<>();
}
}
} catch (Throwable t) {
mThrowable = t;
} finally {
// 唤醒等待锁的线程,可以读取值了。可能读取的是旧值
mLock.notifyAll();
}
}
}

SharedPreferences 初始化时开启线程加载本地 xml 文件,如果文件较大,比较耗时,可能主线程读取值时因为获取不到锁,一直等待,造成卡顿或 ANR。

保存数据

1
2
3
SharedPreferences preferences = getSharedPreferences("sp_name", Context.MODE_PRIVATE);
preferences.edit().putString("key", "value").commit();
preferences.edit().putString("key", "value").apply();

提交数据的两种方式:

  1. **commit()**:同步提交,会阻塞调用它的线程,并且这个方法会返回 boolean 值告知保存是否成功。

  2. **apply()**:异步提交,无返回值,使用 HandlerThread 异步保存到文件;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// SharedPreferencesImpl.java
@Override
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}

public final class EditorImpl implements Editor {
private final Map<String, Object> mModified = new HashMap<>();

@Override
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
}

edit() 时会创建 EditorImpl 对象,包含 mModified 的 Map 集合,用来保存修改的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// SharedPreferencesImpl.java
public final class EditorImpl implements Editor {

@Override
public boolean commit() {
// 修改内存中的值
MemoryCommitResult mcr = commitToMemory();
// 保存数据到文件
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
mcr.writtenToDiskLatch.await();
notifyListeners(mcr);
return mcr.writeToDiskResult;
}

@Override
public void apply() {
// 修改内存中的值
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
mcr.writtenToDiskLatch.await();
}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};

// 保存数据到文件
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
}
  • 同步提交时,先提交到内存,接着在调用者线程执行写文件任务;
  • 异步提交时,先提交到内存,把任务添加到队列,使用 HandlerThread 串行执行写文件任务;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// SharedPreferencesImpl.java
// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
Map<String, Object> mapToWriteToDisk;

synchronized (SharedPreferencesImpl.this.mLock) {
synchronized (mEditorLock) {
// 保存原来的数据
mapToWriteToDisk = mMap;

// 清空 SP
if (mClear) {
if (!mapToWriteToDisk.isEmpty()) {
mapToWriteToDisk.clear();
}
}

// 遍历需要修改的 Map 对象
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
if (v == this || v == null) {
if (!mapToWriteToDisk.containsKey(k)) {
continue;
}
mapToWriteToDisk.remove(k);
} else {
// 原来的集合中存在此 key
if (mapToWriteToDisk.containsKey(k)) {
// 并且 value 也相等,不需要更新
Object existingValue = mapToWriteToDisk.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
// 不存在此 key,则添加到原来的 Map 中
mapToWriteToDisk.put(k, v);
}
}
mModified.clear();
}
}
return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
listeners, mapToWriteToDisk);
}

根据修改的集合,更新老集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// SharedPreferencesImpl.java
private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
// 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();
return;
}
}

// 异步提交时将任务添加到队列,使用 HandlerThread 串行处理
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
boolean fileExists = mFile.exists();
if (fileExists) {
boolean backupFileExists = mBackupFile.exists();
// 备份文件不存在,先把备份 xml 文件
if (!backupFileExists) {
if (!mFile.renameTo(mBackupFile)) {
mcr.setDiskWriteResult(false, false);
return;
}
} else {
// 备份文件存在,删除 SP 文件
mFile.delete();
}
}

// 创建 xml 文件
FileOutputStream str = createFileOutputStream(mFile);
// 将内存中的数据保存到文件
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
// 关闭输出流
str.close();
final StructStat stat = Os.stat(mFile.getPath());
synchronized (mLock) {
// 重新计算时间戳和文件大小
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
}

// 文件写入成功,删除备份文件
mBackupFile.delete();
}
  1. 写入文件时,如果备份文件不存在,先把备份文件;
  2. 将内存中的数据保存到文件;
  3. 重新计算时间戳和文件大小,删除备份文件;

当一个进程正处于写文件的过程中的时候,如果另一个进程读文件,才会看到 mBackupFile, 这时候读进程会将 mBackupFile 重命名为 mFile, 这样读结果是,读进程只能都到修改前的文件,同时,由于 mBackupFile 重命名为了 mFile, 所以写进程写那个文件就没有文件名引用了,因此其写入的内容无法再被任何进程访问到。所以其内容丢失了,可认为写入失败了,而 SharedPreferences 对这种失败情况没有任何重试机制,所以就可能出现数据丢失的情况。

MODE_MULTI_PROCESS 模式下,每次获取 SharedPreferences 都会检测文件是否改变,只要读的时候另一进程在写,就会导致写丢失。

获取数据

1
2
SharedPreferences preferences = getSharedPreferences("sp_name", Context.MODE_PRIVATE);
String result = preferences.getString("key", null);
1
2
3
4
5
6
7
8
@Override
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
  • 获取到对象锁后,再从内存中获取数据。

多进程问题

如果 SP 对象的 mode 指定了 Context.MODE_MULTI_PROCESS,那么当前进程会先从 xml 文件中重新加载,再读取值。

如果前一个进程由于数据比较大,写入文件比较耗时,当前的进程读取发生在前一个进程写成功之前,即使当前进程指定了 MODE_MULTI_PROCESS 标记,也是无法读取到值的。所以 SharedPreferences 是非进程安全的,而且 MODE_MULTI_PROCESS 标记已经废弃,推荐使用 ContentProvider 等支持跨进程的方式。

1
2
3
4
5
6
7
8
9
10
// ContextImpl.java
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
// 指定 MODE_MULTI_PROCESS 会重新加载 SP 文件
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}

多进程访问主要有两种情况:

  1. 不同 apk(不同 packageName,且不具有相同 uid)的多进程:
    由于 linux 文件权限是根据 uid 设置访问权限,因此必须设置 mode 为 MODE_WORLD_READABLE 或 MODE_WORLD_WRITABLE,取决于别的应用是否有需要该 SharedPreferences,由于这种方式需要修改文件权限,且会让所有人都具访问权限,无法只对某个应用授权,所以非常危险,android N 上对 targetsdk >= 24(7.0) 的应用已明确禁止这两个 mode。
  2. 同一个 packageName 或具有相同 uid 的 package 里面存在多个进程:
    这种情况下多个进程具有相同的 uid,因此不需要修改文件权限,没有权限安全性问题。目前很多 apk 都支持多进程,例如后台服务与前台页面运行在独立的进程。

总结

  1. SharedPreferences 每次写入操作,首先是将源文件备份;
  2. 写入文件成功后才删除备份文件。通过 HandlerThread 写文件;
  3. 如果写入过程中进程被杀或关机等非正常情况发生,进程再次启动后如果发现该 SharedPreferences 存在 Backup 文件,就将 Backup 文件重名为源文件,原本未完成写入的文件就直接丢弃,这样就能保证之前数据的正确。

参考

[1] Android 之不要滥用 SharedPreferences(上)
[2] Android 之不要滥用 SharedPreferences(下)
[3] Tencent/MMKV
[4] SharedPreferences 多进程问题