我们经常使用的SharedPreferences其实是存在很多缺陷的,主要表现在
- 占用内存
- getValue时可能导致ANR
- 不支持多进程
- 不支持局部更新
- commit或apply都可能导致ANR
以下参考安卓源码的基础上,使用大白话和部分代码片段和大家一起探讨分享。
占用内存
final class SharedPreferencesImpl implements SharedPreferences {
......
//构造方法
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
//从磁盘里获取xml里的数据
startLoadFromDisk();
}
.....
}
我们都知道Context的上下文实现是依靠ContextImpl这个类,而我们的SharedPreferences的实现是依靠SharedPreferencesImpl类,
ContextImpl.java
/**
* Map from package name, to preference name, to cached preferences.
*/
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
在我们的ContextImpl类中存在一个静态的ArrayMap对象用于缓存当前packageName下的所有sp文件对象,
但是在这个类里面我们可以看到缓存数组的探空 初始化和赋值,但却没有对数组对象里的数据进行移除或者释放的操作,
由此我们也就可以知道,在我们APP运行的过程中,APP对应包目录下的sp文件都会被缓存到方法区里去,
而这种机制的话会导致很占内存,而且宁愿OOM也不会主动释放内存空间。
getValue的时候可能导致线程阻塞或ANR
在我们的SharedPreferencesImpl构造函数里,会启动一个子线程去加载磁盘文件,把xml文件转换成map对象,如果文件很大或者线程调度没有马上启动这个线程的话,那么这个加载的操作需要一段时间后才能执行完成,
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
而假如我们刚好初始化的时候紧接着去getValue的话,getValue里面又会通过awaitLoadedLocked方法来校验是否要阻塞外部线程,
private void awaitLoadedLocked() {
if (!mLoaded) {
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
//如果没有加载完成 就一直持有锁
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
确保取值操作前一定是执行完成了file文件的加载和转换成功,最后在磁盘加载完成时才会notify操作 把我们外部读取value的线程给唤醒。
在上述的操作场景都是我们APP经常会出现的,同时当我们sp离数据存储量很大的话,那这个磁盘加载并阻塞外部线程的时间会比较大 直接就导致了我们主线程获取sp值的时候直接就芭比Q anr了。
不支持多进程
名义上我们在获取sp实例的时候可以传参支持多进程模式,但这个mode参数也只是起到一个多进程数据同步的作用,
static void setFilePermissionsFromMode(String name, int mode,
int extraPermissions) {
int perms = FileUtils.S_IRUSR|FileUtils.S_IWUSR
|FileUtils.S_IRGRP|FileUtils.S_IWGRP
|extraPermissions;
if ((mode&MODE_WORLD_READABLE) != 0) {
perms |= FileUtils.S_IROTH;
}
if ((mode&MODE_WORLD_WRITEABLE) != 0) {
perms |= FileUtils.S_IWOTH;
}
FileUtils.setPermissions(name, perms, -1, -1);
}
这里的同步是指访问这个sp实例的时候,会判断当前磁盘文件相对最后一次内存修改是否被改动过,如果是的话就重新加载磁盘文件再同步到缓存上,
public static int setPermissions(String path, int mode, int uid, int gid) {
try {
Os.chmod(path, mode);
} catch (ErrnoException e) {
Slog.w(TAG, "Failed to chmod(" + path + "): " + e);
return e.errno;
}
if (uid >= 0 || gid >= 0) {
try {
Os.chown(path, uid, gid);
} catch (ErrnoException e) {
Slog.w(TAG, "Failed to chown(" + path + "): " + e);
return e.errno;
}
}
return 0;
}
但这种同步的作用不大,因为当多进程同时修改sp值,但不同进程里的内存数据也不会实时同步,而且同时修改sp数据也会导致数据丢失和覆盖的可能。
不支持局部更新
apply
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
//这个任务最终在ActivityThread里的 handleStopService handlePauseActivity handleStopActivity方法里执行
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
// 最终调用QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
//把这个任务加入到ActivityThread中的QueueWork列表里
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
// changes reflected in memory.
notifyListeners(mcr);
}
我们的同步修改commit方法 和异步修改apply方法都是全量更新,也就是即使我们修改的止损一个键值对,它也会把数据重写写入到磁盘文件中,这样就会导致不必要的内存开销。
commit或apply都可能导致ANR
在commit和apply的时候还有一个更致命的问题就是他们也会导致ANR。
这个主要是因为在调用commit和apply都会执行到一个enqueueDiskWrite操作,这个操作会把当前修改sp内存数据同步到Disk磁盘的任务加入到ActivityThread里的一个任务链表集合中, 那么我们肯定会想这个磁盘同步任务什么时候才会最终完成呢,
其实它是需要等到我们的应用中service在stop的时候,或者activity暂停或停止的时候,才会for循环上面提到的任务链表集合任务,最终完成内存数据到磁盘数据的。 那这样的话会因为有大量的读写同步到磁盘的任务导致activity或者service切换生命周期的时候被阻塞住了,最终导致了ANR。
--》handleStopActivity方法(ActivityThread)
--》QueuedWork.waitToFinish()
--》 processPendingWork(); 再到下面最终执行磁盘回写任务
for (Runnable w : work) {
w.run();
}
综上,经过这些分析想必我们对SharedPreferences有个更了解的地方。
安卓官方推荐我们可以考虑使用jetpack里的DataStore ,或者可以考虑使用腾讯团队开发的MMKV框架。