前言
文章中部分地方SharedPreferences会简写成SP,先抛出几个问题:
- SP存储的是什么文件,存储在哪个位置?
- SP是线程安全的吗?
- SP是如何保证数据安全的?
- 使用SP有哪些问题?
- SP会把数据加载到内存中吗?
- 首次使用SP和第二次使用SP,关于加载数据这块会有哪些不同?
- 使用SP存储json会有问题吗?
- SP有没有备份机制?
- SP可以跨进程吗?
上面问题有笔者亲身经历过的面试题,也有网上找的,确实如果没有看过SP源码的话,第一次面对这些问题真的会一脸懵逼。下面我们结合源码看下SP,顺便也找找这些问题的答案。
关于SP一些基础的使用和问题可以看我以前的一篇文章:Android数据存储之SharedPreferences详细总结
基本使用
写入:
SharedPreferences settings = getSharedPreferences("test", MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
editor.putString("name", "张飞");
editor.putInt("age", 18);
//editor.commit();
editor.apply();
读取:
SharedPreferences settings = getSharedPreferences("test", MODE_PRIVATE);
String name = settings.getString("name","");
int age = settings.getInt("age",0);
源码分析
使用入口
首先我们去看SharedPreferences.java代码,SharedPreferences仅仅是一个接口,并没有实现相关的代码。那具体的实现在哪里呢?我们去看getSharedPreferences(“test”, MODE_PRIVATE)方法是怎么获取到SharedPreferences的
//ContextWrapper.java
public SharedPreferences getSharedPreferences(String name, int mode) {
return mBase.getSharedPreferences(name, mode);
}
ContextWrapper交给了mBase调用getSharedPreferences,对Context体系比较熟悉的同学应该知道这个mBase是一个ContextImpl,我们直接去看ContextImpl
public SharedPreferences getSharedPreferences(String name, int mode) {
// At least one application in the world actually passes in a null
// name. This happened to work because when we generated the file name
// we would stringify it to "null.xml". Nice.
if (mPackageInfo.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
if (name == null) {
name = "null";
}
}
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
//在SharedPreferences详细总结文章中有说SharedPreferences存储的是文件
//这里就是查找或者生成那个文件,mSharedPrefsPaths是一个ArrayMap
//用于缓存文件路径和文件
file = mSharedPrefsPaths.get(name);
if (file == null) {
//没有找到,根据路径生成文件并存储在mSharedPrefsPaths中
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
下面接着去看getSharedPreferences方法,传递进去了文件和使用SharedPreferences的模式
public SharedPreferences getSharedPreferences(File file, int mode) {
//SharedPreferences真正的实现
SharedPreferencesImpl sp;
//线程安全的获取
synchronized (ContextImpl.class) {
//还是从缓存中取,文件为键,SharedPreferencesImpl为值
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
//检查mode,Android N及以上MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE已经不支持
checkMode(mode);
if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
if (isCredentialProtectedStorage()
&& !getSystemService(UserManager.class)
.isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
throw new IllegalStateException("SharedPreferences in credential encrypted "
+ "storage are not available until after user is unlocked");
}
}
//为空的话创建并写入缓存
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
//Android 11及以上不支持使用MODE_MULTI_PROCESS
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;
}
可见核心逻辑都在SharedPreferencesImpl,下面接着去看SharedPreferencesImpl的代码
SharedPreferencesImpl
SharedPreferencesImpl构造函数
SharedPreferencesImpl(File file, int mode) {
//传入的对应路径的file赋值给mFile
mFile = file;
//看名字是一个备份用的文件
mBackupFile = makeBackupFile(file);
//记录模式
mMode = mode;
mLoaded = false;
//mMap设置为空
mMap = null;
mThrowable = null;
//从存储中加载
startLoadFromDisk();
}
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
//new 了一个线程去加载文件
loadFromDisk();
}
}.start();
}
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
...
Map<String, Object> map = null;
StructStat stat = null;
Throwable thrown = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
//根据文件生成BufferedInputStream流,用于读取
str = new BufferedInputStream(
new FileInputStream(mFile), 16 * 1024);
//XmlUtils从流中读取内容并写入map,会生成一个HashMap
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) {
//在synchronized代码块中把上面加载好的map赋值给mMap
//至此数据就从xml文件中读取到内存中了
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
//大概是读取失败,赋值一个空的HashMap
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();
}
}
}
总结:
- 关于XML解析这块我们只需要知道,XML解析方式有Dom,Sax,还有Pull解析。这里使用了Pull解析。
- xml读取是耗时的,如果我们在SP中存储大量的数据必然会导致耗时增加
- 首次会加载到内存中,第二次就不会了
- 在内存中存储格式为Map<String, Object>数据形式
edit()
SharedPreferences并不允许直接putInt,putString必须先通过edit()获取一个Editor,然后调用Editor的各种putInt,putString等方法,然后在调用apply或者commit方法执行存入操作。下面我们分析下edit()方法
public Editor edit() {
// TODO: remove the need to call awaitLoadedLocked() when
// requesting an editor. will require some work on the
// Editor, but then we should be able to do:
//
// context.getSharedPreferences(..).edit().putString(..).apply()
//
// ... all without blocking.
synchronized (mLock) {
//可以看下面代码,就是等待加载完成,也就是等待loadFromDisk执行完成
awaitLoadedLocked();
}
//返回一个新建的EditorImpl
return new EditorImpl();
}
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);
}
}
EditorImpl
Editor真正的实现是EditorImpl
public final class EditorImpl implements Editor {
//Editor的锁
private final Object mEditorLock = new Object();
//修改的数据
@GuardedBy("mEditorLock")
private final Map<String, Object> mModified = new HashMap<>();
@GuardedBy("mEditorLock")
private boolean mClear = false;
@Override
public Editor putString(String key, @Nullable String value) {
//put 方法加锁
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
//各种put方法,所有的put方法都类似都加锁
...
@Override
public Editor remove(String key) {
//根据键移除一个,加锁
synchronized (mEditorLock) {
mModified.put(key, this);
return this;
}
}
@Override
public Editor clear() {
//清空,加锁
synchronized (mEditorLock) {
mClear = true;
return this;
}
}
@Override
public void apply() {...}
@Override
public void commit() {...}
}
apply
我们都知道Editor有两个写入数据的方法,一个是apply异步写入非阻塞,一个是commit是阻塞的。但是现实情况真的是这样吗,我们来看看。
@Override
public void apply() {
final long startTime = System.currentTimeMillis();
//执行commitToMemory,并返回一个MemoryCommitResult包括写入内存的结果
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
//MemoryCommitResult有一个writtenToDiskLatch
//只有setDiskWriteResult方法执行的时候才会countDown
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
if (DEBUG && mcr.wasWritten) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " applied after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
//执行写入存储的操作,后面分析
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);
}
//看方法名,顾名思义是先提交到内存中
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
boolean keysCleared = false;
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
Map<String, Object> mapToWriteToDisk;
//以当前SharedPreferencesImpl对象的mLock为锁
synchronized (SharedPreferencesImpl.this.mLock) {
// We optimistically don't make a deep copy until
// a memory commit comes in when we're already
// writing to disk.
//是否已经有写操作在执行,是的话复制一份,操作自己复制的这一份
if (mDiskWritesInFlight > 0) {
// We can't modify our mMap as a currently
// in-flight write owns it. Clone it before
// modifying it.
// noinspection unchecked
//写入前先复制一份,并赋值给mMap
mMap = new HashMap<String, Object>(mMap);
}
mapToWriteToDisk = mMap;
//在执行的写操作数+1
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
keysModified = new ArrayList<String>();
listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
synchronized (mEditorLock) {
boolean changesMade = false;
if (mClear) {
//是清空的话,这里对mapToWriteToDisk执行清空操作
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
mapToWriteToDisk.clear();
}
keysCleared = true;
mClear = false;
}
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
// "this" is the magic value for a removal mutation. In addition,
// setting a value to "null" for a given key is specified to be
// equivalent to calling remove on that key.
if (v == this || v == null) {
if (!mapToWriteToDisk.containsKey(k)) {
continue;
}
//值为null,清空这个key
mapToWriteToDisk.remove(k);
} else {
if (mapToWriteToDisk.containsKey(k)) {
Object existingValue = mapToWriteToDisk.get(k);
//前后值相同,不处理
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
//更新到内存中
mapToWriteToDisk.put(k, v);
}
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
//将mModified清空,以备下次使用
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
//返回一个MemoryCommitResult,包裹结果,包括监听者和写入后的map
return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
listeners, mapToWriteToDisk);
}
MemoryCommitResult
private static class MemoryCommitResult {
final long memoryStateGeneration;
final boolean keysCleared;
@Nullable final List<String> keysModified;
@Nullable final Set<OnSharedPreferenceChangeListener> listeners;
final Map<String, Object> mapToWriteToDisk;
final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
@GuardedBy("mWritingToDiskLock")
volatile boolean writeToDiskResult = false;
boolean wasWritten = false;
private MemoryCommitResult(long memoryStateGeneration, boolean keysCleared,
@Nullable List<String> keysModified,
@Nullable Set<OnSharedPreferenceChangeListener> listeners,
Map<String, Object> mapToWriteToDisk) {
this.memoryStateGeneration = memoryStateGeneration;
this.keysCleared = keysCleared;
this.keysModified = keysModified;
this.listeners = listeners;
this.mapToWriteToDisk = mapToWriteToDisk;
}
void setDiskWriteResult(boolean wasWritten, boolean result) {
this.wasWritten = wasWritten;
writeToDiskResult = result;
writtenToDiskLatch.countDown();
}
}
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
//isFromSyncCommit是否来自同步的commit,很明显不是,这里postWriteRunnable
//不为空
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
//写入xml文件
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) {
//如果是来自commit的话,看mDiskWritesInFlight是否是1
//为1也就表示没有其他的commit或者apply在执行,只有当前
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
//直接执行writeToDiskRunnable
writeToDiskRunnable.run();
return;
}
}
//这里是apply,所以来到这里通过QueuedWork添加这个writeToDiskRunnable
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
关于QueuedWork还有一个隐藏的问题,等会在其他里面介绍。
//QueuedWork.java
public static void queue(Runnable work, boolean shouldDelay) {
//新建一个HandlerThread,其实就是放在子线程中执行
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 Handler getHandler() {
synchronized (sLock) {
if (sHandler == null) {
HandlerThread handlerThread = new HandlerThread("queued-work-looper",
Process.THREAD_PRIORITY_FOREGROUND);
handlerThread.start();
sHandler = new QueuedWorkHandler(handlerThread.getLooper());
}
return sHandler;
}
}
总结:
- apply会放在子线程执行写入存储
- Editor写入,读取内存,读取存储,等有三把锁保证线程安全。所以SP是线程安全的。
commit
看完了apply的分析,其实commit剩余的已经没啥东西了
@Override
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
//和apply一样先写入内存
MemoryCommitResult mcr = commitToMemory();
//执行enqueueDiskWrite,注意后面参数传入的是null表示直接执行
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");
}
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
总结:
commit和apply都会先写入内存,然后再写入存储,区别是写入存储commit是在当前线程,apply是在子线程执行。
writeToFile
前面分析了apply和commit并没有深入看文件的写入文件这块,继续看下
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
long startTime = 0;
long existsTime = 0;
long backupExistsTime = 0;
long outputStreamCreateTime = 0;
long writeTime = 0;
long fsyncTime = 0;
long setPermTime = 0;
long fstatTime = 0;
long deleteTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
//文件是否存在
boolean fileExists = mFile.exists();
if (DEBUG) {
existsTime = System.currentTimeMillis();
// Might not be set, hence init them to a default value
backupExistsTime = existsTime;
}
// Rename the current file so it may be used as a backup during the next read
//如果文件存在的话将当前文件改名,以备下次读取的时候当备份文件使用
if (fileExists) {
boolean needsWrite = false;
// Only need to write if the disk state is older than this commit
if (mDiskStateGeneration < mcr.memoryStateGeneration) {
if (isFromSyncCommit) {
//来自commit的话直接设置为true
needsWrite = true;
} else {
synchronized (mLock) {
// No need to persist intermediate states. Just wait for the latest state to
// be persisted.
if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
needsWrite = true;
}
}
}
}
if (!needsWrite) {
mcr.setDiskWriteResult(false, true);
return;
}
//备份文件是否存在
boolean backupFileExists = mBackupFile.exists();
if (DEBUG) {
backupExistsTime = System.currentTimeMillis();
}
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();
}
}
// Attempt to write the file, delete the backup and return true as atomically as
// possible. If any exception occurs, delete the new file; next time we will restore
// from the backup.
try {
FileOutputStream str = createFileOutputStream(mFile);
if (DEBUG) {
outputStreamCreateTime = System.currentTimeMillis();
}
if (str == null) {
mcr.setDiskWriteResult(false, false);
return;
}
//通过XmlUtils写入xml文件
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
writeTime = System.currentTimeMillis();
FileUtils.sync(str);
fsyncTime = System.currentTimeMillis();
//关闭流
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
if (DEBUG) {
setPermTime = System.currentTimeMillis();
}
try {
final StructStat stat = Os.stat(mFile.getPath());
synchronized (mLock) {
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
}
} catch (ErrnoException e) {
// Do nothing
}
if (DEBUG) {
fstatTime = System.currentTimeMillis();
}
// Writing was successful, delete the backup file if there is one.
//写入成功,备份文件已经没有存在的必要了,下次需要的时候读取新的或者直接重命名当前文件
mBackupFile.delete();
if (DEBUG) {
deleteTime = System.currentTimeMillis();
}
mDiskStateGeneration = mcr.memoryStateGeneration;
//设置写入结果
mcr.setDiskWriteResult(true, true);
...
long fsyncDuration = fsyncTime - writeTime;
mSyncTimes.add((int) fsyncDuration);
mNumSync++;
return;
} catch (XmlPullParserException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
} catch (IOException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
}
// Clean up an unsuccessfully written file
if (mFile.exists()) {
if (!mFile.delete()) {
Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
}
}
mcr.setDiskWriteResult(false, false);
}
其他
SP备份机制
其实看完writeToFile这个方法我们应该已经知道了SP的备份机制,每次读取SP的时候都会创建一个被备份文件,每次写入文件前,先看下这个备份文件是否存在,不存在将当前指向xml的文件改名为备份文件,正常SharedPreferencesImpl构造方法里面已经根据这个xml文件创建了备份文件。
writeToFile方法执行成功会把备份文件删除,这个时候xml文件已经是新的了。异常情况写入文件失败,比如掉电,强制退出等。写入新的SP会失败那么下次loadFromDisk会把备份文件设置为mFile,也就是读取旧的内容,保证不至于失败导致数据被污染。
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
...
}
QueuedWork隐藏问题
刚才说apply还隐藏了一个刺客在哪里呢,在QueuedWork里面,这里参考了字节的文章今日头条 ANR 优化实践系列 - 告别 SharedPreference 等待
public void handlePauseActivity(ActivityClientRecord r, boolean finished, boolean userLeaving,
int configChanges, PendingTransactionActions pendingActions, String reason) {
...
// Make sure any pending writes are now committed.
if (r.isPreHoneycomb()) {
//Android 11以前的版本,等待waitToFinish执行完成,所以这里会导致Activity一直等待
//QueuedWork把任务执行完成,也就是隐藏会导致卡顿或者ANR
QueuedWork.waitToFinish();
}
...
}
存储json
参考请不要滥用SharedPreference
还有一些童鞋,他在sp里面存json或者HTML;这么做不是不可以,但是,如果这个json相对较大,那么也会引起sp读取速度的急剧下降。
JSON或者HTML格式存放在sp里面的时候,需要转义,这样会带来很多&这种特殊符号,sp在解析碰到这个特殊符号的时候会进行特殊的处理,引发额外的字符串拼接以及函数调用开销。而JSON本来就是可以用来做配置文件的,你干嘛又把它放在sp里面呢?多此一举。下面我写个demo验证一下。
参考链接
https://juejin.cn/post/6961961476047568932?searchId=20240504152606909D2A140930E1DACD5E
https://juejin.cn/post/6884505736836022280?searchId=20240503211729C6AE6135B0EDD86DCF0C
https://weishu.me/2016/10/13/sharedpreference-advices/