Android 开源库

⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。

当一个开发者或团队的水平积累到一定程度,会有自内向外输出价值的需求。在这个专栏里,小彭将为你分享 Android 方向主流开源组件的实现原理,包括网络、存储、UI、监控等。

本文是 Android 开源库系列的第 2 篇文章,完整文章目录请移步到文章末尾~

前言

我们继续上一篇文章的分析:


  1. 两种写回策略

在获得事务对象后,我们继续分析 Editor 接口中的 commit 同步写回策略和 apply 异步写回策略。

6.1 commit 同步写回策略

Editor#commit 同步写回相对简单,核心步骤分为 4 步:

  • 1、调用 commitToMemory() 创建 MemoryCommitResult 事务对象;
  • 2、调用 enqueueDiskWrite(mrc, null) 提交磁盘写回任务(在当前线程执行);
  • 3、调用 CountDownLatch#await() 阻塞等待磁盘写回完成;
  • 4、调用 notifyListeners() 触发回调监听。

commit 同步写回示意图

其实严格来说,commit 同步写回也不绝对是在当前线程同步写回,也有可能在后台 HandlerThread 线程写回。但不管怎么样,对于 commit 同步写回来说,都会调用 CountDownLatch#await() 阻塞等待磁盘写回完成,所以在逻辑上也等价于在当前线程同步写回。

SharedPreferencesImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public final class EditorImpl implements Editor {

@Override
public boolean commit() {
// 1、获取事务对象(前文已分析)
MemoryCommitResult mcr = commitToMemory();
// 2、提交磁盘写回任务
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null /* 写回成功回调 */);
// 3、阻塞等待写回完成
mcr.writtenToDiskLatch.await();
// 4、触发回调监听器
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
}

6.2 apply 异步写回策略

Editor#apply 异步写回相对复杂,核心步骤分为 5 步:

  • 1、调用 commitToMemory() 创建 MemoryCommitResult 事务对象;
  • 2、创建 awaitCommit Ruunnable 并提交到 QueuedWork 中。awaitCommit 中会调用 CountDownLatch#await() 阻塞等待磁盘写回完成;
  • 3、创建 postWriteRunnable Runnable,在 run() 中会执行 awaitCommit 任务并将其从 QueuedWork 中移除;
  • 4、调用 enqueueDiskWrite(mcr, postWriteRunnable) 提交磁盘写回任务(在子线程执行);
  • 5、调用 notifyListeners() 触发回调监听。

可以看到不管是调用 commit 还是 apply,最终都会调用 SharedPreferencesImpl#enqueueDiskWrite() 提交磁盘写回任务。

区别在于:

  • 在 commit 中 enqueueDiskWrite() 的第 2 个参数是 null;
  • 在 apply 中 enqueueDiskWrite() 的第 2 个参数是一个 postWriteRunnable 写回结束的回调对象,enqueueDiskWrite() 内部就是根据第 2 个参数来区分 commit 和 apply 策略。

apply 异步写回示意图

SharedPreferencesImpl.java

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
java复制代码@Override
public void apply() {
// 1、获取事务对象(前文已分析)
final MemoryCommitResult mcr = commitToMemory();
// 2、提交 aWait 任务
// 疑问:postWriteRunnable 可以理解,awaitCommit 是什么?
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
// 阻塞线程直到磁盘任务执行完毕
mcr.writtenToDiskLatch.await();
}
};
QueuedWork.addFinisher(awaitCommit);
// 3、创建写回成功回调
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
// 执行 aWait 任务
awaitCommit.run();
// 移除 aWait 任务
QueuedWork.removeFinisher(awaitCommit);
}
};

// 4、提交磁盘写回任务,并绑定写回成功回调
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable /* 写回成功回调 */);

// 5、触发回调监听器
notifyListeners(mcr);
}

QueuedWork.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码// 提交 aWait 任务(后文详细分析)
private static final LinkedList<Runnable> sFinishers = new LinkedList<>();

public static void addFinisher(Runnable finisher) {
synchronized (sLock) {
sFinishers.add(finisher);
}
}

public static void removeFinisher(Runnable finisher) {
synchronized (sLock) {
sFinishers.remove(finisher);
}
}

这里有一个疑问:

在 apply() 方法中,在执行 enqueueDiskWrite() 前创建了 awaitCommit 任务并加入到 QueudWork 等待队列,直到磁盘写回结束才将 awaitCommit 移除。这个 awaitCommit 任务是做什么的呢?

我们稍微再回答,先继续往下走。

6.3 enqueueDiskWrite() 提交磁盘写回事务

可以看到,不管是 commit 还是 apply,最终都会调用 SharedPreferencesImpl#enqueueDiskWrite() 提交写回磁盘任务。虽然 enqueueDiskWrite() 还没到真正调用磁盘写回操作的地方,但确实创建了与磁盘 IO 相关的 Runnable 任务,核心步骤分为 4 步:

  • 步骤 1:根据是否有 postWriteRunnable 回调区分是 commit 和 apply;
  • 步骤 2:创建磁盘写回任务(真正执行磁盘 IO 的地方):
    • 2.1 调用 writeToFile() 执行写回磁盘 IO 操作;
    • 2.2 在写回结束后对前文提到的 mDiskWritesInFlight 计数自减 1;
    • 2.3 执行 postWriteRunnable 写回成功回调;
  • 步骤 3:如果是异步写回,则提交到 QueuedWork 任务队列;
  • 步骤 4:如果是同步写回,则检查 mDiskWritesInFlight 变量。如果存在并发写回的事务,则也要提交到 QueuedWork 任务队列,否则就直接在当前线程执行。

其中步骤 2 是真正执行磁盘 IO 的地方,逻辑也很好理解。不好理解的是,我们发现除了 “同步写回而且不存在并发写回事务” 这种特殊情况,其他情况都会交给 QueuedWork 再调度一次。

在通过 QueuedWork#queue 提交任务时,会将 writeToDiskRunnable 任务追加到 sWork 任务队列中。如果是首次提交任务,QueuedWork 内部还会创建一个 HandlerThread 线程,通过这个子线程实现异步的写回任务。这说明 SharedPreference 的异步写回相当于使用了一个单线程的线程池,事实上在 Android 8.0 以前的版本中就是使用一个 singleThreadExecutor 线程池实现的。

提交任务示意图

SharedPreferencesImpl.java

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
java复制代码private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
// 1、根据是否有 postWriteRunnable 回调区分是 commit 和 apply
final boolean isFromSyncCommit = (postWriteRunnable == null);
// 2、创建磁盘写回任务
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
// 2.1 写入磁盘文件
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
// 2.2 mDiskWritesInFlight:进行中事务自减 1
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
// 2.3 触发写回成功回调
postWriteRunnable.run();
}
}
};

// 3、同步写回且不存在并发写回,则直接在当前线程
// 这就是前文提到 “commit 也不是绝对在当前线程同步写回” 的源码出处
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
// 如果存在并发写回的事务,则此处 wasEmpty = false
wasEmpty = mDiskWritesInFlight == 1;
}
// wasEmpty 为 true 说明当前只有一个线程在执行提交操作,那么就直接在此线程上完成任务
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}

// 4、交给 QueuedWork 调度(同步任务不可以延迟)
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit /*是否可以延迟*/ );
}

@GuardedBy("mWritingToDiskLock")
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
// 稍后分析
}

QueuedWork 调度:

QueuedWork.java

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
java复制代码@GuardedBy("sLock")
private static LinkedList<Runnable> sWork = new LinkedList<>();

// 提交任务
// shouldDelay:是否延迟
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();

synchronized (sLock) {
// 入队
sWork.add(work);
// 发送 Handler 消息,触发 HandlerThread 执行任务
if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY /* 100ms */);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}

private static Handler getHandler() {
synchronized (sLock) {
if (sHandler == null) {
// 创建 HandlerThread 后台线程
HandlerThread handlerThread = new HandlerThread("queued-work-looper", Process.THREAD_PRIORITY_FOREGROUND);
handlerThread.start();

sHandler = new QueuedWorkHandler(handlerThread.getLooper());
}
return sHandler;
}
}

private static class QueuedWorkHandler extends Handler {
static final int MSG_RUN = 1;

QueuedWorkHandler(Looper looper) {
super(looper);
}

public void handleMessage(Message msg) {
if (msg.what == MSG_RUN) {
// 执行任务
processPendingWork();
}
}
}

private static void processPendingWork() {
synchronized (sProcessingWork) {
LinkedList<Runnable> work;

synchronized (sLock) {
// 创建新的任务队列
// 这一步是必须的,否则会与 enqueueDiskWrite 冲突
work = sWork;
sWork = new LinkedList<>();

// Remove all msg-s as all work will be processed now
getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
}

// 遍历 ,按顺序执行 sWork 任务队列
if (work.size() > 0) {
for (Runnable w : work) {
w.run();
}
}
}
}

比较不理解的是:

同一个文件的多次写回串行化可以理解,对于多个文件的写回串行化意义是什么,是不是可以用多线程来写回多个不同的文件?或许这也是 SharedPreferences 是轻量级框架的原因之一,你觉得呢?

6.4 主动等待写回任务结束

现在我们可以回答 6.1 中遗留的问题:

在 apply() 方法中,在执行 enqueueDiskWrite() 前创建了 awaitCommit 任务并加入到 QueudWork 等待队列,直到磁盘写回结束才将 awaitCommit 移除。这个 awaitCommit 任务是做什么的呢?

要理解这个问题需要管理分析到 ActivityThread 中的主线程消息循环:

可以看到,在主线程的 Activity#onPause、Activity#onStop、Service#onStop、Service#onStartCommand 等生命周期状态变更时,会调用 QueudeWork.waitToFinish():

ActivityThread.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Override
public void handlePauseActivity(...) {
performPauseActivity(r, finished, reason, pendingActions);
// Make sure any pending writes are now committed.
if (r.isPreHoneycomb()) {
QueuedWork.waitToFinish();
}
...
}

private void handleStopService(IBinder token) {
...
QueuedWork.waitToFinish();
ActivityManager.getService().serviceDoneExecuting(token, SERVICE_DONE_EXECUTING_STOP, 0, 0);
...
}

waitToFinish() 会执行所有 sFinishers 等待队列中的 aWaitCommit 任务,主动等待所有磁盘写回任务结束。在写回任务结束之前,主线程会阻塞在等待锁上,这里也有可能发生 ANR。

主动等待示意图

至于为什么 Google 要在 ActivityThread 中部分生命周期中主动等待所有磁盘写回任务结束呢?官方并没有明确表示,结合头条和抖音技术团队的文章,我比较倾向于这 2 点解释:

  • 解释 1 - 跨进程同步(主要): 为了保证跨进程的数据同步,要求在组件跳转前,确保当前组件的写回任务必须在当前生命周期内完成;
  • 解释 2 - 数据完整性: 为了防止在组件跳转的过程中可能产生的 Crash 造成未写回的数据丢失,要求当前组件的写回任务必须在当前生命周期内完成。

当然这两个解释并不全面,因为就算要求主动等待,也不能保证跨进程实时同步,也不能保证不产生 Crash。

抖音技术团队观点

QueuedWork.java

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
java复制代码@GuardedBy("sLock")
private static Handler sHandler = null;

public static void waitToFinish() {
boolean hadMessages = false;

Handler handler = getHandler();

synchronized (sLock) {
if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
// Delayed work will be processed at processPendingWork() below
handler.removeMessages(QueuedWorkHandler.MSG_RUN);
}
// We should not delay any work as this might delay the finishers
sCanDelay = false;
}

// Android 8.0 优化:帮助子线程执行磁盘写回
// 作用有限,因为 QueuedWork 使用了 sProcessingWork 锁保证同一时间最多只有一个线程在执行磁盘写回
// 所以这里应该是尝试在主线程执行,可以提升线程优先级
processPendingWork();

// 执行 sFinshers 等待队列,等待所有写回任务结束
try {
while (true) {
Runnable finisher;

synchronized (sLock) {
finisher = sFinishers.poll();
}

if (finisher == null) {
break;
}
// 执行 mcr.writtenToDiskLatch.await();
// 阻塞线程直到磁盘任务执行完毕
finisher.run();
}
} finally {
sCanDelay = true;
}
}

Android 7.1 QueuedWork 源码对比:

1
2
3
java复制代码public static boolean hasPendingWork() {
return !sPendingWorkFinishers.isEmpty();
}

  1. writeToFile() 姗姗来迟

最终走到具体调用磁盘 IO 操作的地方了!

7.1 写回步骤

writeToFile() 的逻辑相对复杂一些了。经过简化后,剩下的核心步骤只有 4 大步骤:

  • 步骤 1:过滤无效写回事务:
+ 1.1 事务的 memoryStateGeneration 内存版本小于 mDiskStateGeneration 磁盘版本,跳过;
+ 1.2 同步写回必须写回;
+ 1.3 异步写回事务的 memoryStateGeneration 内存版本版本小于 mCurrentMemoryStateGeneration 最新内存版本,跳过。
  • 步骤 2:文件备份:
+ 2.1 如果不存在备份文件,则将旧文件重命名为备份文件;
+ 2.2 如果存在备份文件,则删除无效的旧文件(上一次写回出并且后处理没有成功删除的情况)。
  • 步骤 3:全量覆盖写回磁盘:
+ 3.1 打开文件输出流;
+ 3.2 将 mapToWriteToDisk 映射表全量写出;
+ 3.3 调用 FileUtils.sync() 强制操作系统页缓存写回磁盘;
+ 3.4 写入成功,则删除备份文件(如果没有走到这一步,在将来读取文件时,会重新恢复备份文件);
+ 3.5 将磁盘版本记录为当前内存版本;
+ 3.6 写回结束(成功)。
  • 步骤 4:后处理: 删除写至半途的无效文件。

7.2 写回优化

继续分析发现,SharedPreference 的写回操作并不是简单的调用磁盘 IO,在保证 “可用性” 方面也做了一些优化设计:

  • 优化 1 - 过滤无效的写回事务:

如前文所述,commit 和 apply 都可能出现并发修改同一个文件的情况,此时在连续修改同一个文件的事务序列中,旧的事务是没有意义的。为了过滤这些无意义的事务,在创建 MemoryCommitResult 事务对象时会记录当时的 memoryStateGeneration 内存版本,而在 writeToFile() 中就会根据这个字段过滤无效事务,避免了无效的 I/O 操作。

  • 优化 2 - 备份旧文件:

由于写回文件的过程存在不确定的异常(比如内核崩溃或者机器断电),为了保证文件的完整性,SharedPreferences 采用了文件备份机制。在执行写回操作之前,会先将旧文件重命名为 .bak 备份文件,在全量覆盖写入新文件后再删除备份文件。

如果写回文件失败,那么在后处理过程中会删除写至半途的无效文件。此时磁盘中只有一个备份文件,而真实文件需要等到下次触发写回事务时再写回。

如果直到应用退出都没有触发下次写回,或者写回的过程中 Crash,那么在前文提到的创建 SharedPreferencesImpl 对象的构造方法中调用 loadFromDisk() 读取并解析文件数据时,会从备份文件恢复数据。

  • 优化 3 - 强制页缓存写回:

在写回文件成功后,SharedPreference 会调用 FileUtils.sync() 强制操作系统将页缓存写回磁盘。

写回示意图

SharedPreferencesImpl.java

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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
java复制代码// 内存版本
@GuardedBy("this")
private long mCurrentMemoryStateGeneration;

// 磁盘版本
@GuardedBy("mWritingToDiskLock")
private long mDiskStateGeneration;

// 写回事务
private static class MemoryCommitResult {
// 内存版本
final long memoryStateGeneration;
// 需要全量覆盖写回磁盘的数据
final Map<String, Object> mapToWriteToDisk;
// 同步计数器
final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);

// 后文写回结束后调用
// wasWritten:是否有执行写回
// result:是否成功
void setDiskWriteResult(boolean wasWritten, boolean result) {
this.wasWritten = wasWritten;
writeToDiskResult = result;
// 唤醒等待锁
writtenToDiskLatch.countDown();
}
}

// 提交写回事务
private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
...
// 创建磁盘写回任务
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
// 2.1 写入磁盘文件
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
// 2.2 mDiskWritesInFlight:进行中事务自减 1
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
// 2.3 触发写回成功回调
postWriteRunnable.run();
}
}
};
...
}

// 写回文件
// isFromSyncCommit:是否同步写回
@GuardedBy("mWritingToDiskLock")
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
boolean fileExists = mFile.exists();
// 如果旧文件存在
if (fileExists) {
// 1. 过滤无效写回事务
// 是否需要执行写回
boolean needsWrite = false;

// 1.1 磁盘版本小于内存版本,才有可能需要写回
// (只有旧文件存在才会走到这个分支,但是旧文件不存在的时候也可能存在无意义的写回,
// 猜测官方是希望首次创建文件的写回能够及时尽快执行,毕竟只有一个后台线程)
if (mDiskStateGeneration < mcr.memoryStateGeneration) {
if (isFromSyncCommit) {
// 1.2 同步写回必须写回
needsWrite = true;
} else {
// 1.3 异步写回需要判断事务对象的内存版本,只有最新的内存版本才有必要执行写回
synchronized (mLock) {
if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
needsWrite = true;
}
}
}
}

if (!needsWrite) {
// 1.4 无效的异步写回,直接结束
mcr.setDiskWriteResult(false, true);
return;
}

// 2. 文件备份
boolean backupFileExists = mBackupFile.exists();
if (!backupFileExists) {
// 2.1 如果不存在备份文件,则将旧文件重命名为备份文件
if (!mFile.renameTo(mBackupFile)) {
// 备份失败
mcr.setDiskWriteResult(false, false);
return;
}
} else {
// 2.2 如果存在备份文件,则删除无效的旧文件(上一次写回出并且后处理没有成功删除的情况)
mFile.delete();
}
}

try {
// 3、全量覆盖写回磁盘
// 3.1 打开文件输出流
FileOutputStream str = createFileOutputStream(mFile);
if (str == null) {
// 打开输出流失败
mcr.setDiskWriteResult(false, false);
return;
}
// 3.2 将 mapToWriteToDisk 映射表全量写出
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
// 3.3 FileUtils.sync:强制操作系统将页缓存写回磁盘
FileUtils.sync(str);
// 关闭输出流
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);

// 3.4 写入成功,则删除备份文件(如果没有走到这一步,在将来读取文件时,会重新恢复备份文件)
mBackupFile.delete();
// 3.5 将磁盘版本记录为当前内存版本
mDiskStateGeneration = mcr.memoryStateGeneration;
// 3.6 写回结束(成功)
mcr.setDiskWriteResult(true, true);

return;
} catch (XmlPullParserException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
} catch (IOException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
}

// 在 try 块中抛出异常,会走到这里
// 4、后处理:删除写至半途的无效文件
if (mFile.exists()) {
if (!mFile.delete()) {
Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
}
}
// 写回结束(失败)
mcr.setDiskWriteResult(false, false);
}

// -> 读取并解析文件数据
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) {
return;
}
// 1、如果存在备份文件,则恢复备份数据(后文详细分析)
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
...
}

至此,SharedPreferences 核心源码分析结束。


  1. SharedPreferences 的其他细节

SharedPreferences 还有其他细节值得学习。

8.1 SharedPreferences 锁总结

SharedPreferences 是线程安全的,但它的线程安全并不是直接使用一个全局的锁对象,而是采用多种颗粒度的锁对象实现 “锁细化” ,而且还贴心地使用了 @GuardedBy 注解标记字段或方法所述的锁级别。

使用 @GuardedBy 注解标记锁级别

1
2
java复制代码@GuardedBy("mLock")
private Map<String, Object> mMap;
对象锁 功能 描述
1、SharedPreferenceImpl#mLock SharedPreferenceImpl 对象的全局锁 全局使用
2、EditorImpl#mEditorLock EditorImpl 修改器的写锁 确保多线程访问 Editor 的竞争安全
3、SharedPreferenceImpl#mWritingToDiskLock SharedPreferenceImpl#writeToFile() 的互斥锁 writeToFile() 中会修改内存状态,需要保证多线程竞争安全
4、QueuedWork.sLock QueuedWork 的互斥锁 确保 sFinishers 和 sWork 的多线程资源竞争安全
5、QueuedWork.sProcessingWork QueuedWork#processPendingWork() 的互斥锁 确保同一时间最多只有一个线程执行磁盘写回任务

8.2 使用 WeakHashMap 存储监听器

SharedPreference 提供了 OnSharedPreferenceChangeListener 回调监听器,可以在主线程监听键值对的变更(包含修改、新增和移除)。

SharedPreferencesImpl.java

1
2
3
java复制代码@GuardedBy("mLock")
private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners =
new WeakHashMap<OnSharedPreferenceChangeListener, Object>();

SharedPreferences.java

1
2
3
4
5
6
java复制代码public interface SharedPreferences {

public interface OnSharedPreferenceChangeListener {
void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);
}
}

比较意外的是: SharedPreference 使用了一个 WeakHashMap 弱键散列表存储监听器,并且将监听器对象作为 Key 对象。这是为什么呢?

这是一种防止内存泄漏的考虑,因为 SharedPreferencesImpl 的生命周期是全局的(位于 ContextImpl 的内存缓存),所以有必要使用弱引用防止内存泄漏。想想也对,Java 标准库没有提供类似 WeakArrayList 或 WeakLinkedList 的容器,所以这里将监听器对象作为 WeakHashMap 的 Key,就很巧妙的复用了 WeakHashMap 自动清理无效数据的能力。

提示: 关于 WeakHashMap 的详细分析,请阅读小彭说 · 数据结构与算法 专栏文章 《WeakHashMap 和 HashMap 的区别是什么,何时使用?》

8.3 如何检查文件被其他进程修改?

在读取和写入文件后记录 mStatTimestamp 时间戳和 mStatSize 文件大小,在检查时检查这两个字段是否发生变化

SharedPreferencesImpl.java

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
java复制代码// 文件时间戳
@GuardedBy("mLock")
private StructTimespec mStatTimestamp;
// 文件大小
@GuardedBy("mLock")
private long mStatSize;

// 读取文件
private void loadFromDisk() {
...
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
...
}

// 写入文件
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
...
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
...
}

// 检查文件
private boolean hasFileChangedUnexpectedly() {
synchronized (mLock) {
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;
}
}

// 读取文件 Stat 信息
final StructStat stat = Os.stat(mFile.getPath());

synchronized (mLock) {
// 检查修改时间和文件大小
return !stat.st_mtim.equals(mStatTimestamp) || mStatSize != stat.st_size;
}
}

至此,SharedPreferences 全部源码分析结束。


  1. 总结

可以看到,虽然 SharedPreferences 是一个轻量级的 K-V 存储框架,但的确是一个完整的存储方案。从源码分析中,我们可以看到 SharedPreferences 在读写性能、可用性方面都有做一些优化,例如:锁细化、事务化、事务过滤、文件备份等,值得细细品味。

在下篇文章里,我们来盘点 SharedPreferences 中存在的 “缺点”,为什么 SharedPreferences 没有乘上新时代的船只。请关注。


版权声明

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

参考资料

推荐阅读

Android 开源库系列完整目录如下(2023/07/12 更新):

⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%