原理

SharedPreferences 存取值原理,其实还是将数据写入到xml文件 以及 缓存中。
Context.getSharedPreferences 都是在ContextImpl中实现,但是在API23之前、23之后实现方式却不同

API 23

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
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
// 第一次为空时,初始化值
if (sSharedPrefs == null) {
sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
}
// 获取对应packageName下的 sp实例集合 也是当前应用第一次使用时初始化
final String packageName = getPackageName();
ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();
sSharedPrefs.put(packageName, packagePrefs);
}
if (mPackageInfo.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
if (name == null) {
name = "null";
}
}
// 根据传入的name获取对应的sp实例,第一次初始化 获取对应的文件
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) {
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}

@Override
public File getSharedPrefsFile(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}

ContextImpl 中维护有一个静态集合 sSharedPrefs = ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>
sSharedPrefs 全局唯一

该map中 key为 应用包名,value 为一个 packagePrefs ArrayMap<String, SharedPreferencesImpl>,

SharedPreferencesImpl 是单个sp实例信息,文件、数据map缓存

packagePrefs 存有一个应用中所有的 sp实例 SharedPreferencesImpl

sSharedPrefs 存有所有应用关于sp的实例信息

存取值其实就是在集合sSharedPrefs中通过包名packageName获取到 packagePrefs,再根据 初入的name获取到指定的 SharedPreferencesImpl实例,然后再通过实例来读写数据

getSharedPrefsFile 就是返回 应用data文件夹/shared_prefs/ name.xml

API 24及以上

ContextImp 中存在两个 getSharedPreferences函数,
public SharedPreferences getSharedPreferences(String name, int mode);
public SharedPreferences getSharedPreferences(File file, int mode);

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
private ArrayMap<String, File> mSharedPrefsPaths;
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

@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) {
// 获取name 对应的文件
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) {
// 获取当前运行应用的 sp集合
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");
}
}
// 新建 SharedPreferencesImpl 对象
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;
}

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;
}

在api24以后,ContextImpl中不再维护有静态的sSharedPrefs集合,
而是维护有一个ArrayMap mSharedPrefsPaths ,以及一个静态集合ArrayMap sSharedPrefsCache
sSharedPrefs key为name,value为文件
sSharedPrefsCache ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> key为包名packageName, value 为集合ArrayMap<File, SharedPreferencesImpl>

getSharedPreferences(String name, int mode)函数中,先通过name获取到对应的文件,再调用public SharedPreferences getSharedPreferences(File file, int mode);

API 23 与 24 的差异

从上面来看,API23与24差异很小,其实就是将内部的集合的key从 string 改成了 file,
在API24中,增多一个mSharedPrefsPaths集合,在集合中就有 name 与 file的映射关系。
对于需要频繁获取的sp实例来说,可能略有优化,但是也增加了内存消耗。

##

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
@UnsupportedAppUsage
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk();
}

@UnsupportedAppUsage
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;
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);
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 时,会初始化一些变量,并且执行startLoadFromDisk
在startLoadFromDisk 中会新开线程执行 loadFromDisk
在loadFromDisk 中,会删除原文件,然后将 备份文件重命名

然后就是通过文件流读取 文件信息,将读取到的信息赋值给 SharedPreferencesImpl中的 map对象。
同时对文件的读取都是加锁操作的。当文件读取完成了,执行mLock.notifyAll();唤醒所有操作线程。

loadFromDisk 需要新开线程也是互斥的问题,必须保证load 与读写不在同一线程,才能让不会一直await,在加载完能够唤醒读写的操作继续。

getValue()

内部有针对不同类型的get方法,基本都一致,看一个就可以了。

1
2
3
4
5
6
7
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}

读操作也是加锁的,防止读、写同时,导致数据异常,同时也跟上面的 loadFromDisk 中加锁呼应,防止问价还未加载完就进行读写操作

put

put操作需要通过内部类EditorImpl来完成。

1
2
3
4
5
6
7
8
9
 @Override
public Editor edit() {
// 当文件未加载完,即loadFromDisk未执行完时,会一直等待。
synchronized (mLock) {
awaitLoadedLocked();
}
// 每次获取edit时都是重新创建一个对象。
return new EditorImpl();
}

每次获取Edit对象时都是返回一个新的对象,所以尽量将数据操作合并,不要频繁去重新获取edit对象。
在看一下put数据的方法

1
2
3
4
5
6
7
8
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;
}
}

执行put方法时,只是将数据提交到 EditorImpl 中的一个HashMap中,
只有在commit 或者 apply时,才会将数据合并、写入到文件中。

  • QA:为何要设计一个mModified,来保存数据,而不是直接提交合并到文件?
    这样可以避免频繁操作文件,只有在执行commit、apply时才去操作文件,提高效率,是一种优化手段。

commit

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
@Override
public boolean commit() {
long startTime = 0;

if (DEBUG) {
startTime = System.currentTimeMillis();
}
// 合并mModified数据到一个新的集合,并清除mModified数据,并记录哪些key的value发生更改,最后将合并的数据包装成一个MemoryCommitResult对象
MemoryCommitResult mcr = commitToMemory();
// 将mcr加入文件写入队列,注意第二个参数为null,标示 直接写入,不需等待
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;
}

apply

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
@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);

notifyListeners(mcr);
}

可以看到 apply、commit两个函数基本相同,主要时在 enqueueDiskWrite 函数执行时,传入的第二个参数不同

enqueueDiskWrite

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
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
// commit时,传入的postWriteRunnable为null, isFromSyncCommit 为true,
// apply时 postWriteRunnable != null isFromSyncCommit = false

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();
}
}
};

// commit 才会进入这个判断,并最终执行writeToDiskRunnable 然后return
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
// apply 会执行此处
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

commit、apply差异

从上面的注释也可以看出:

  • commit会直接在当前线程执行 writeToDiskRunnable.run();
  • 而 apply 会将 writeToDiskRunnable 加入队列 QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);等待线程池执行任务。

总结

SP是线程安全的,通过锁、await、notifyAll,保证并行时不会读写异常。

SP通过全局静态ArrayMap维护一个集合,通过packageName、name找到对应的读写文件file、SPImpl实例。

读操作是加载file完之后,直接在缓存的一个集合Map中根据key读取即可。

写操作是先将需要写入的数据都缓存到一个HashMap中,再在commit或者apply时与file中的数据合并,并标示哪些key发生改变,包装成一个MemoryCommitResult对象。

写操作只是修改缓存的HashMap,修改持久化的数据还需要执行commit或者apply。

commit 是当前线程直接执行,而 apply是添加到任务队列等待线程池执行。

优化建议

  • 不要存放大的key和value在SharedPreferences中,否则会一直存储在内存中得不到释放,内存使用过高会频发引发GC,导致界面丢帧甚至ANR。

  • 不相关的配置选项最好不要放在一起,单个文件越大读取速度则越慢。

  • 读取频繁的key和不频繁的key尽量不要放在一起(如果整个文件本身就较小则忽略,为了这点性能添加维护得不偿失)。

  • 不要每次都edit,因为每次都会创建一个新的EditorImpl对象,最好是批量处理统一提交。否则edit().commit每次创建一个EditorImpl对象并且进行一次IO操作,严重影响性能。

  • commit发生在UI线程中,apply发生在工作线程中,对于数据的提交最好是批量操作统一提交。虽然apply发生在工作线程(不会因为IO阻塞UI线程)但是如果添加任务较多也有可能带来其他严重后果
    (参照ActivityThread源码中handleStopActivity方法实现)

  • 尽量不要存放json和html,这种可以直接文件缓存。

  • 不要指望它能够跨进程通信 Context.PROCESS

  • 最好提前初始化SharedPreferences,避免SharedPreferences第一次创建时读取文件线程未结束而出现等待情况。