如果我的文章对您有所帮助,欢迎 👍 , 我的Github.
1. 前言
SharedPreferences(以下简称sp)在android中比较常用的工具类,在使用过程中还是有一些需要注意的坑,我们来浅析下源码。
本文sp基于anrdroid-28.
2. 浅析
2.1 sp对象创建
-> android.app.ContextImpl line:408
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
checkMode(mode);
...
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
...
return sp;
}
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
...
// 重载方法,使用默认路径 /package/shared_prefs/$name.xml
return getSharedPreferences(file, mode);
}
-> android.app.Activity#getPreferences
// getLocalClassName() = $activityClassName.xml
public SharedPreferences getPreferences(@Context.PreferencesMode int mode) {
return getSharedPreferences(getLocalClassName(), mode);
}
先从缓存中看是否存在spImpl,没有就会重新创建一个。这个地方加了synchronized (ContextImpl.class)防止多线程中多次创建实例。
2.2 mode
// sp文件权限
// android.content.Context#MODE_PRIVATE 私有(用户组内)
// android.content.Context#MODE_WORLD_READABLE 公开读
// android.content.Context#MODE_WORLD_WRITEABLE 公开写
// android.content.Context#MODE_APPEND 同 MODE_PRIVATE
// android.content.Context#MODE_MULTI_PROCESS 同 MODE_PRIVATE,会判断其他进程是否需要同步文件(不可靠)
int mode
// getSharedPreferences时检查
// MODE_WORLD_READABLE & MODE_WORLD_WRITEABLE在Build.VERSION_CODES.N以后已被废弃
private void checkMode(int mode) {
if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {
if ((mode & MODE_WORLD_READABLE) != 0) {
throw new SecurityException("MODE_WORLD_READABLE no longer supported");
}
if ((mode & MODE_WORLD_WRITEABLE) != 0) {
throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");
}
}
}
// sp在写入文件的时候根据mode设置了文件权限
-> android.app.SharedPreferencesImpl#writeToFile line:782
-> android.app.ContextImpl#setFilePermissionsFromMode
static void setFilePermissionsFromMode(String name, int mode,
int extraPermissions) {
// S_xxx 这些是linux中文件权限flag
// 默认 同用户&用户组可读可写
int perms = FileUtils.S_IRUSR|FileUtils.S_IWUSR
|FileUtils.S_IRGRP|FileUtils.S_IWGRP
|extraPermissions;
// 给与其他可读权限,Build.VERSION_CODES.N后不支持
if ((mode&MODE_WORLD_READABLE) != 0) {
perms |= FileUtils.S_IROTH;
}
// 给与其他可写权限,Build.VERSION_CODES.N后不支持
if ((mode&MODE_WORLD_WRITEABLE) != 0) {
perms |= FileUtils.S_IWOTH;
}
...
FileUtils.setPermissions(name, perms, -1, -1);
}
通过mode控制文件的访问权限
2.3 sp缓存更新
-> android.app.ContextImpl line:408
public SharedPreferences getSharedPreferences(File file, int mode) {
...
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
// If somebody else (some other process) changed the prefs
// file behind our back, we reload it. This has been the
// historical (if undocumented) behavior.
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
-> android.app.ContextImpl line:454
@Override
public void reloadSharedPreferences() {
// Build the list of all per-context impls (i.e. caches) we know about
ArrayList<SharedPreferencesImpl> spImpls = new ArrayList<>();
...
// Issue the reload outside the cache lock
for (int i = 0; i < spImpls.size(); i++) {
spImpls.get(i).startReloadIfChangedUnexpectedly();
}
}
-> android.app.SharedPreferencesImpl line:198
void startReloadIfChangedUnexpectedly() {
synchronized (mLock) {
// TODO: wait for any pending writes to disk?
if (!hasFileChangedUnexpectedly()) {
return;
}
startLoadFromDisk();
}
}
如果mode包含MODE_MULTI_PROCESS,sp会在初始化时判断文件是否需要更新,但这不是可靠的。 如果sp文件发生变化时,会通过 reloadSharedPreferences进行更新缓存
2.4 sp文件加载解析
// 构造函数和startReloadIfChangedUnexpectedly中会加载解析sp文件
SharedPreferencesImpl(File file, int mode) {
...
startLoadFromDisk();
}
void startReloadIfChangedUnexpectedly(){
...
startLoadFromDisk();
}
// 锁住,并开启一个线程
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
private void loadFromDisk() {
...
// 如果备份文件存在,说明上次写入动作失败,会让备份文件转正
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
...
// 解析xml,其中value可能是set or list or map
map = (Map<String, Object>) XmlUtils.readMapXml(str);
...
// 同步文件信息,以便文件变化时判断
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
...
// 释放锁
mLock.notifyAll();
}
在解析sp文件的时候会通过mLock锁锁住其他操作(解析文件、getXXX)
在回写sp文件的时候会备份一下,如果回写失败了,下次读的时候会直接使用备份文件
2.5 sp文件回写
@Override
public Editor edit() {
...
return new EditorImpl();
}
@Override
public void apply() {
...
// 修改内存中的值
final MemoryCommitResult mcr = commitToMemory();
// mcr.writtenToDiskLatch 不太明白这给锁的作用,
// 不管commit还是apply mcr.writtenToDiskLatch都是在执行完writeToFile才await的
// 回写任务,apply会丢到QueuedWork中的线程执行
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
...
}
@Override
public boolean commit() {
// 修改内存中的值
MemoryCommitResult mcr = commitToMemory();
...
// 与apply的区别在于这个方法是同步执行的(当前线程)
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
...
}
private MemoryCommitResult commitToMemory() {
//android.app.SharedPreferencesImpl.EditorImpl#mModified putXXX时会记录在这个map中
// sp数据版本号,如果数据发生改变,这个值会+1
// 每个sp有一个mDiskStateGeneration版本号,文件回写成功会将其置为memoryStateGeneration
// 写入文件的时候比mDiskStateGeneration值小的内存数据将遗弃
long memoryStateGeneration;
// 遍历android.app.SharedPreferencesImpl.EditorImpl#mModified与当前sp数据的,记录其中有变化的值
List<String> keysModified = null;
...
// 最终写入文件的完整数据
Map<String, Object> mapToWriteToDisk;
...
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
...
writeToFile(mcr, isFromSyncCommit);
...
}
};
...
// commit 和 apply的区别,在于是否同步执行 writeToFile
if (isFromSyncCommit) {
...
writeToDiskRunnable.run();
...
}
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
@GuardedBy("mWritingToDiskLock")
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
...
boolean fileExists = mFile.exists();
// sp文件存在时判断下是否需要写入(版本号),是否需要备份(防止写入过程错误,导致文件损坏)
if (fileExists) {
boolean needsWrite = false;
// 比较当前要写入的版本与disk的版本,看是否需要写入
if (mDiskStateGeneration < mcr.memoryStateGeneration) {
if (isFromSyncCommit) {
needsWrite = true;
} else {
// apply方式存在多线程可能,如果sp内存版本号改变就遗弃当前操作,相当于有一定的节流
synchronized (mLock) {
if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
needsWrite = true;
}
}
}
}
if (!needsWrite) {
mcr.setDiskWriteResult(false, true);
return;
}
boolean backupFileExists = mBackupFile.exists();
// 备份文件不存在将当前文件转为备份文件
if (!backupFileExists) {
if (!mFile.renameTo(mBackupFile)) {
Log.e(TAG, "Couldn't rename file " + mFile
+ " to backup file " + mBackupFile);
mcr.setDiskWriteResult(false, false);
return;
}
} else {
mFile.delete();
}
}
FileOutputStream str = createFileOutputStream(mFile);
...
// 将内存数据回写sp文件
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
...
// 根据mode,设置文件权限
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
...
final StructStat stat = Os.stat(mFile.getPath());
// 同步文件信息,以便文件变化时判断
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
...
// 干掉备份文件,下次回写开始时重新备份
mBackupFile.delete();
...
// 同步disk文件版本号
mDiskStateGeneration = mcr.memoryStateGeneration;
mcr.setDiskWriteResult(true, true);
...
}
edit操作每次都会返回一个新的对象,所以put操作要做合并处理。 apply相比commit会使用QueuedWork的线程写入文件,并且在写入文件的时候会锁住getXXX操作 apply操作可能会失败,并没有回调。
2.6 sp加密
android-23(6.0+)之后可以使用Jitpack提供的implementation "androidx.security:security-crypto:$version"库获取一个加密的sp对象
EncryptedSharedPreferences是对SharedPreferences的一个包装类,getXXX和putXXX时会对key-value进行加解密(加密的密钥会保存在android密钥库中)
String masterKey = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC);
SharedPreferences sp = EncryptedSharedPreferences.create(
"secret_sp",
masterKey,
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
);
2.7 相关class
- android.content.Context
- android.content.SharedPreferences
- (hide) android.app.SharedPreferencesImpl
- (hide) android.app.ContextImpl
- (hide) android.os.FileUtils
- (hide) com.android.internal.util.XmlUtils
3. 总结
-
项目中的SpUtils不需要再对sp对象进行缓存,sp自身已经做了缓存。
-
Sp不适合再多进程中共享,这是不稳定的方案。
-
sp中读写都会加锁,有大量读写操作的尽量不要使用sp,或者对读写进行文件拆分、edit批量提交。