⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。
当一个开发者或团队的水平积累到一定程度,会有自内向外输出价值的需求。在这个专栏里,小彭将为你分享 Android 方向主流开源组件的实现原理,包括网络、存储、UI、监控等。
本文是 Android 开源库系列的第 2 篇文章,完整文章目录请移步到文章末尾~
前言
我们继续上一篇文章的分析:
- Android 开源库 #1 初代 K-V 存储框架 SharedPreferences,旧时代的余晖?(上)
- Android 开源库 #2 初代 K-V 存储框架 SharedPreferences,旧时代的余晖?(下)
6. 两种写回策略
在获得事务对象后,我们继续分析 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
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
@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
// 提交 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
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
@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
@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
@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 源码对比:
public static boolean hasPendingWork() {
return !sPendingWorkFinishers.isEmpty();
}
7. 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
// 内存版本
@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 核心源码分析结束。
8. SharedPreferences 的其他细节
SharedPreferences 还有其他细节值得学习。
8.1 SharedPreferences 锁总结
SharedPreferences 是线程安全的,但它的线程安全并不是直接使用一个全局的锁对象,而是采用多种颗粒度的锁对象实现 “锁细化” ,而且还贴心地使用了 @GuardedBy
注解标记字段或方法所述的锁级别。
使用 @GuardedBy 注解标记锁级别
@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
@GuardedBy("mLock")
private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners =
new WeakHashMap<OnSharedPreferenceChangeListener, Object>();
SharedPreferences.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
// 文件时间戳
@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 全部源码分析结束。
9. 总结
可以看到,虽然 SharedPreferences 是一个轻量级的 K-V 存储框架,但的确是一个完整的存储方案。从源码分析中,我们可以看到 SharedPreferences 在读写性能、可用性方面都有做一些优化,例如:锁细化、事务化、事务过滤、文件备份等,值得细细品味。
在下篇文章里,我们来盘点 SharedPreferences 中存在的 “缺点”,为什么 SharedPreferences 没有乘上新时代的船只。请关注。
版权声明
本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
参考资料
- Android SharedPreferences 的理解与使用 —— ghroosk 著
- 一文读懂 SharedPreferences 的缺陷及一点点思考 —— 业志陈 著
- 反思|官方也无力回天?Android SharedPreferences 的设计与实现 —— 却把青梅嗅 著
- 剖析 SharedPreference apply 引起的 ANR 问题 —— 字节跳动技术团队
- 今日头条 ANR 优化实践系列 - 告别 SharedPreference 等待 —— 字节跳动技术团队
推荐阅读
Android 开源库系列完整目录如下(2023/07/12 更新):
⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~