androidGreenHand
2023/04/13阅读:20主题:全栈蓝
android SP源码
SP
❝❞
轻量级储存 以键值对(key-value)的方式保存数据的xml文件,其保存在/data/data/shared_prefs目录下
项目中已迁移使用MMKV或者DataStore,为什么还热衷于SP的源码呢?
❝有没有可能知道我不会MMKV或者DataStore源码呢,我想大概是的
❞
TODO MMKV或者DataStore原理
读操作
❝当SharedPreferences对象第一次通过Context.getSharedPreferences()进行初始化时,对xml文件进行一次读取,并将文件内所有内容(即所有的键值对)缓到内存的一个Map中
❞
final class SharedPreferencesImpl implements SharedPreferences {
private final File mFile; // 对应的xml文件
private Map<String, Object> mMap; // Map中缓存了xml文件中所有的键值对
}
备注:详细的过程,ANR部分再解析
写操作
❝抽象出了一个Editor类,不管某次操作通过若干次调用putXXX()方法,更新了xml中的几个键值对,只有调用了commit()方法,最终才会真正写入文件
❞
commit 调用线程写操作
**commit 源码**:
@Override
public boolean commit() {
```
MemoryCommitResult mcr = commitToMemory();//内存保存
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null /* sync write on this thread okay */);//第二个参数为null
```
return mcr.writeToDiskResult;
}
可以看到enqueueDiskWrite的第二个参数为null
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null);//此时postWriteRunnable为null,isFromSyncCommit 则为true
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) { //当调用commit方法时,isFromSyncCommit则为true
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();//主线程回调writeToDiskRunnable的run方法,进行writeToFile文件的存储
return;
}
}
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
postWriteRunnable为null,则isFromSyncCommit为true,代码会在主线程回调writeToDiskRunnable的run方法,进行writeToFile文件的存储。这部分动作直接在主线程执行,如果文件较大,则主线程也会因为IO时间长造成ANR的。
apply 异步线程写操作
❝apply()方法设计的初衷是为了规避主线程的I/O操作导致ANR问题的产生,那么ANR的问题真得到了有效的解决吗?
❞
「apply 源码」:
public void apply() {
```
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
};
// 将 awaitCommit 添加到队列 QueuedWork 中
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);// 将 awaitCommit 从队列 QueuedWork 中移除
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
```
}
apply方法最后调用了SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable)
enqueueDiskWrite方法会把存储文件的动作放到子线程,这个方法的第二个参数postWriteRunnable做了两件事:
-
让awaitCommit执行 及执行 mcr.writtenToDiskLatch.await(); -
执行QueuedWork.remove(awaitCommit);
CountDownLatch
❝writtenToDiskLatch的类型是CountDownLatch,CountDownLatch是一个同步工具类,通过一个计数器来实现的同步。当一个线程完成了自己的任务调用countDown(),则计数器的值就相应得减1。当计数器到达0时,表示所有的线程都已执行完毕,然后在等待的线程await()就可以恢复执行任务。
❞
-
countDown(): 对计数器进行递减1操作,当计数器递减至0时,当前线程会去唤醒阻塞队列里的所有线程。
-
await(): 阻塞当前线程,将当前线程加入阻塞队列。
可以看到如果postWriteRunnable方法被触发执行的话,由于 mcr.writtenToDiskLatch.await()的缘故,UI线程会被一直阻塞住,等待计数器减至0才能被唤醒。
enqueueDiskWrite源码如下所示:
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
很明显postWriteRunnable不为null,程序会执行QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit)
;
writeToDiskRunnable:
-
writeToFile(): 内容存储到文件 -
postWriteRunnable.run()
QueuedWork.queue源码:
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();
synchronized (sLock) {
sWork.add(work);
if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
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();
}
}
}
就是把writeToDiskRunnable这个任务放到sWork这个list中,并且执行handler,根据HandlerThread的知识点,我们知道handlermessage里面就是子线程了。 接下来我们继续看handleMessage里面的processPendingWork()方法:
private static void processPendingWork() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
synchronized (sProcessingWork) {
LinkedList<Runnable> work;
synchronized (sLock) {
work = (LinkedList<Runnable>) sWork.clone();
sWork.clear();
// Remove all msg-s as all work will be processed now
getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
}
if (work.size() > 0) {
for (Runnable w : work) {
w.run();
}
if (DEBUG) {
Log.d(LOG_TAG, "processing " + work.size() + " items took " +
+(System.currentTimeMillis() - startTime) + " ms");
}
}
}
}
是把sWork克隆给work,然后开启循环,执行work对象的run方法,及调用writeToDiskRunnable的run方法。上面讲过了,他里面做了两件事:
-
内容存储到文件 -
postWriteRunnable方法回调
执行run方法的代码:
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);//由于handlermessage在子线程,则writeToFile也在子线程中
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
writeToFile方法我们不深入去看,但是要关注,里面有个setDiskWriteResult方法,在该方法里面做了如下的事情:
void setDiskWriteResult(boolean wasWritten, boolean result) {
this.wasWritten = wasWritten;
writeToDiskResult = result;
writtenToDiskLatch.countDown();//计数器-1
}
当调用countDown()方法时,会对计数器进行递减1操作,当计数器递减至0时,当前线程会去唤醒阻塞队列里的所有线程。
问题:子线程为什么要去执行postWriteRunnable呢?子线程为什么需要await? 也没有看到会出现ANR的情况
数据的更新
❝xml文件中的数据会缓存到内存的mMap中,每次在调用editor.putXXX()时,实际上会将新的数据存入在mMap,当调用commit()或apply()时,最终会将mMap的所有数据全量更新到xml文件里。
❞
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
// name参数就是文件名,通过不同文件名,获取指定的SharedPreferences对象
}
线程安全 ^^3把锁^^
final class SharedPreferencesImpl implements SharedPreferences {
// 1、使用注释标记锁的顺序
// Lock ordering rules:
// - acquire SharedPreferencesImpl.mLock before EditorImpl.mLock
// - acquire mWritingToDiskLock before EditorImpl.mLock
// 2、通过注解标记持有的是哪把锁
@GuardedBy("mLock")
private Map<String, Object> mMap;
@GuardedBy("mWritingToDiskLock")
private long mDiskStateGeneration;
public final class EditorImpl implements Editor {
@GuardedBy("mEditorLock")
private final Map<String, Object> mModified = new HashMap<>();
}
}
「第一把锁」对于简单的 读操作 而言,我们知道其原理是读取内存中mMap的值并返回,那么为了保证线程安全,只需要加一把锁保证mMap的线程安全即可:
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
「保证写操作的线程安全」
❝对于写操作而言,每次putXXX()并不能立即更新在mMap中 ,如果开发者没有调用apply()方法,那么这些数据的更新理所当然应该被抛弃掉,但是如果直接更新在mMap中,那么数据就难以恢复。
❞
因此,Editor本身也应该持有一个mEditorMap对象,用于存储数据的更新;只有当调用apply()时,才尝试将mEditorMap与mMap进行合并,以达到数据更新的目的。
因此,这里我们还需要另外一把锁保证mEditorMap的线程安全,不和mMap公用同一把锁的原因是,在apply()被调用之前,getXXX和putXXX理应是没有冲突的。
public final class EditorImpl implements Editor {
@Override
public Editor putString(String key, String value) {
synchronized (mEditorLock) {
mEditorMap.put(key, value);
return this;
}
}
❝而当真正需要执行apply()进行写操作时,mEditorMap与mMap进行合并,这时必须通过2把锁保证mEditorMap与mMap的线程安全,保证mMap最终能够更新成功,最终向对应的xml文件中进行更新。
❞
「文件的更新理所当然也需要加一把锁:」
// SharedPreferencesImpl.EditorImpl.enqueueDiskWrite()
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
文件损坏 & 备份机制
由于不可预知的原因(比如内核崩溃或者系统突然断电),xml文件的写操作异常中止,Android系统本身的文件系统虽然有很多保护措施,但依然会有数据丢失或者文件损坏的情况。
-
对文件进行备份,SharedPreferences的写入操作正式执行之前,首先会对文件进行备份,将初始文件重命名为增加了一个.bak后缀的备份文件:
// 尝试写入文件
private void writeToFile(...) {
if (!backupFileExists) {
!mFile.renameTo(mBackupFile);
}
}
-
尝试对文件进行写入操作,写入成功时,则将备份文件删除:
// 写入成功,立即删除存在的备份文件
// Writing was successful, delete the backup file if there is one.
mBackupFile.delete();
-
若因异常情况(比如进程被杀)导致写入失败,进程再次启动后,若发现存在备份文件,则将备份文件重名为源文件,原本未完成写入的文件就直接丢弃:
// 从磁盘初始化加载时执行
private void loadFromDisk() {
synchronized (mLock) {
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
}
通过文件备份机制,我们能够保证数据只会丢失最后的更新,而之前成功保存的数据依然能够有效。
进程不安全
没有使用跨进程的锁,SharedPreferences是进程不安全的,在跨进程频繁读写会有数据丢失的可能 问题:这里如何解决?
ANR
写操作时ANR
❝Android系统在页面切换前,将数据写入文件。在ActivityThread的handlePauseActivity和handleStopActivity时会通过waitToFinish保证这些异步任务都已经被执行完成。如果这个时候过渡使用apply方法,则可能导致onpause,onStop执行时间较长,从而导致ANR。
❞
private void handlePauseActivity(IBinder token, boolean finished,
boolean userLeaving, int configChanges, boolean dontReport, int seq) {
......
r.activity.mConfigChangeFlags |= configChanges;
performPauseActivity(token, finished, r.isPreHoneycomb(), "handlePauseActivity");
// Make sure any pending writes are now committed.
if (r.isPreHoneycomb()) {
QueuedWork.waitToFinish();
}
......
}
public static void waitToFinish() {
Handler handler = getHandler();
try {
processPendingWork();
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
try {
while (true) {
Runnable finisher;
synchronized (sLock) {
finisher = sFinishers.poll();
}
if (finisher == null) {
break;
}
finisher.run();
}
} finally {
sCanDelay = true;
}
}
-
主线程执行processPendingWork()方法,把之前未执行完的内容存储到文件的操作执行完,这部分动作直接在主线程执行,如果有未执行的文件操作并且文件较大,则主线程会因为IO时间长造成ANR。 -
循环取出sFinishers数组,执行他的run方法。如果这时候有多个异步线程或者异步线程时间过长,同样会造成阻塞产生ANR。
sFinishers数组是在什么时候add数据的? sFinishers的addFinisher方法是在apply()方法里面调用的,代码如下:
@Override
public void apply() {
......
// 将 awaitCommit 添加到队列 QueuedWork 中
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);// 将 awaitCommit 从队列 QueuedWork 中移除
}
};
......
}
由于UI线程循环取出sFinishers数组,执行他的run方法造成阻塞产生ANR addFinisher刚刚上面提到是在apply方法中调用,入参awaitCommit,他的run方法如下:
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");
}
}
};
不难看出,就是调用CountDownLatch对象的await方法,阻塞当前线程,将当前线程加入阻塞队列。所以如果过渡使用apply方法,则可能导致onpause,onStop执行时间较长从而导致ANR。
首次读操作时ANR
❝sp 文件创建以后,会单独的使用一个线程来加载解析对应的 sp 文件。但是当 UI 线程尝试访问 sp 中内容时,如果 sp 文件还未被完全加载解析到内存,此时 UI 线程会被 block,直到 SP 文件被完全加载到内存中为止
❞
sp 被创建的时候会同时启动一个线程加载对应的 sp 文件,执行 startLoadFromDisk(); 在 startLoadFromDisk()时,标记 sp 不可使用状态,后期无论是尝试读数据或者写数据,读写线程都会被 block,直到 sp 文件被全部加载解析完毕。
源码
@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();
}
sp 文件完全加载解析到内存中,直接唤起所有在等待在当前 sp 的读写线程。
private void loadFromDisk() {
synchronized (mLock) {
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);
}
}
}
synchronized (mLock) {
mLoaded = true;
mLock.notifyAll();
}
}
当UI线程读取SP的值时,可能会发生ANR;
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
@GuardedBy("mLock")
private void awaitLoadedLocked() {
if (!mLoaded) {
// Raise an explicit StrictMode onReadFromDisk for this
// thread, since the real read will be in a different
// thread and otherwise ignored by StrictMode.
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
作者介绍