SharedPreferences是我们在开发中非常常用的,但是应该有很多小伙伴没有去了解过SharedPreferences的实现原理,以及SharedPreferences现在逐渐被谷歌淘汰了,这是为什么呢,让我们一起来研究一下。
1.SharedPreferences的使用
SharedPreferences sp = context.getSharedPreferences("test", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.putString("spKey", "1");
editor.commit();
SharedPreferences sp = context.getSharedPreferences("test2", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.putString("spKey", "1");
editor.apply();
以上是一段简单的SharedPreferences使用,但是细心的小伙伴会发现两段最后一行代码是有一点不一样的,我们来看一下commit的apply有什么区别。
2.SharedPreferences的提交数据时IO写数据解析
(1)我们先来看commit方法,SharedPreferences的实现类是SharedPreferencesImpl,我们看一下里面的commit方法的源码:
@Override
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
// 1.先提交到内存
MemoryCommitResult mcr = commitToMemory();
// 2.真的去IO写入
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");
}
}
// 3.通知回调
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
可以看出commit方法是同步的,也就是说如果在主线程使用commit方法的话在写如何如果很耗时可是会阻塞主线程的有可能到导致ANR,所以我们在使用commit的时候要注意或者尽量使用apply方法,apply是异步的,接下来我们就来看一下apply方法的源码。
(2)apply方法解析
@Override
public void apply() {
final long startTime = System.currentTimeMillis();
// 1.先提交到内存
final MemoryCommitResult mcr = commitToMemory();
// 2.创建写文件的runnable
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");
}
}
};
// 3.添加finisher用于在生命周期结束时判断是否有未执行完的任务
QueuedWork.addFinisher(awaitCommit);
// 4.执行写文件的runnable
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
// 5.异步进行IO写操作
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
// Okay to notify the listeners before it's hit disk
// because the listeners should always get the same
// SharedPreferences instance back, which has the
// changes reflected in memory.
notifyListeners(mcr);
}
可能看完了还是一头雾水,哪里异步了这就需要我们去看enqueueDiskWrite方法的实现了。
(3)enqueueDiskWrite解析
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
// 1.判断是否是同步,commit调用时postWriteRunnable为null
final boolean isFromSyncCommit = (postWriteRunnable == null);
// 2.异步IO写的runnable
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.
// 3. 同步写的时候才执行
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
// 4.是否同步写成功了
wasEmpty = mDiskWritesInFlight == 1;
}
// 5.写失败了就异步再去写
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
// 6.apply异步才执行,QueuedWork利用handler去异步执行runnable
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
看了上面apply一定会有一些疑问,那异步写的话我在写的过程中再次读取我新写的值还能拿到吗,其实是可以的,这就要看SharedPreferences的初始化以及上面代码中提到过的commitToMemory方法了。
3.SharedPreferences的初始化
SharedPreferences初始化的时候会先把sp文件中的数据一次性读出来,保存到内存中,下次再获取的时候就不会再进行IO操作了,直接读取内存即可,减少了IO操作优化了性能,初始化代码如下:
// 1.初始化impl
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk();
}
// 2.异步读取sp文件的数据
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
//3.读取sp xml文件的数据并存储到内存的map中(所以sp文件不宜存过大的数据导致占用内存!)
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();
}
}
}
所以其实我们在使用sp的时候每次只有第一次初始化会涉及IO操作,初始化后读就直接从内存中读取了,例如getString的源码如下:
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
// 加锁等待从sp文件读取数据的过程,自旋等待
awaitLoadedLocked();
// 当IO操作结束后,从内存的map中直接取出
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);
}
}
4.SharedPreferences的线程安全和进程安全
(1)线程安全
SharedPreferences是线程安全的,从上面分析的源码可以看出SharedPreferences在进行操作时都会加锁来保证线程安全,所以线程间是可以放心使用的。
(2)进程安全
SharedPreferences在进程间是不安全的,在跨进程频繁读写会有数据丢失的可能 那么,如何保证SharedPreferences进程的安全呢?
实现思路很多,比如使用文件锁,保证每次只有一个进程在访问这个文件;或者对于Android开发而言,ContentProvider作为官方倡导的跨进程组件,其它进程通过定制的ContentProvider用于访问SharedPreferences,同样可以保证SharedPreferences的进程安全等等。
5.文件损坏 & 备份机制
SharedPreferences再次迎来了新的挑战。
由于不可预知的原因(比如内核崩溃或者系统突然断电),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);
}
}
}
现在,通过文件备份机制,我们能够保证数据只会丢失最后的更新,而之前成功保存的数据依然能够有效。
6.SharedPreferences不可避免的ANR
在上面代码分析中不知道你们有没有记得apply方法中QueuedWork.addFinisher(awaitCommit)这个调用,其实apply是异步安全的,发生ANR是由于Android的另一个机制问题。当Activity.onStop()以及Service处理onStop等相关方法时,则会执行 QueuedWork.waitToFinish()等待所有的等待锁释放,因此如果SharedPreferences一直没有完成更新任务,有可能会导致卡在主线程,最终超时导致ANR。
- 什么情况下SharedPreferences会一直没有完成任务呢? 比如太频繁无节制的apply(),导致任务过多,这也侧面说明了SPUtils.putXXX()这种粗暴的设计的弊端。 这个在头条sp ANR解决方案 的文章中有剖析过这个问题,并且有解决方案,大概就是采用hook在onstop之前把未完成的任务清空,避免onstop时等待而导致的ANR,后面项目中会尝试一下这种方案,当前现在还有替代SharedPreferences的方案可以更好的满足我们的需求。
7.对SharedPreferences的总结和反思
SharedPreferences在我们世纪应用开发时用用非常广泛,所以我们一定要了解SharedPreferences的实现原理,避免踩坑,正确的使用SharedPreferences。
(1)SharedPreferences已经优化了很多,比如一次读取sp后,其他时候使用内存,这种设计思想我们在代码设计的时候也可以做一些参考,极大的减少了IO操作
(2)文件保护机制即使比较常用的bak方法
(3)使用注意事项:避免滥用SharedPreferences,在多次添加sp数据时避免多次commit或者apply。
8.SharedPreferences的替代者
MMKV和Jetpack DataStore是目前主流的替代者,一个是腾讯的开源一个是官方维护的,说明SharedPreferences已经渐渐退出历史舞台了,后面我们有机会再学习一下MMKV和Jetpack DataStore。