一、概述
SharedPreferences
是Android
中一种用来保存相对比较小键值对数据的存储方式,比较适合保存类似用户偏好设置等小数据。SharedPreferences
采用XML
文件格式方式保存数据,文件位于data/data/packagename/shared_prefs
目录下。SharedPreferences
适合轻度使用,重度使用可能导致应用犯病。Android
官方也觉得有问题,已在Jetpack
下研发一个新的解决方案DataStore,以异步、一致的事务方式存储数据,旨在取代 SharedPreferences
,截止目前版本处于1.0.0-alpha04
。本文源码来于API 29
问题:
-
SharedPreferences
的apply()
和commit()
区别? - 会造成
ANR
吗? - 能在多进程下使用吗?
二、简单使用
//保存数据
fun save(view: View) {
val sp = getSharedPreferences("test", Context.MODE_PRIVATE)
val editor = sp.edit()
editor.putString("key", "value")
editor.putInt("num", 1)
editor.apply()
}
//读取数据
fun read(view: View) {
val sp = getSharedPreferences("test", Context.MODE_PRIVATE)
val str = sp.getString("key", "")
val num = sp.getInt("num",0)
println(str)
println(num)
}
三、SP的创建和缓存,ContextImpl分析
SharedPreferences
是一个接口,它的实现类是SharedPreferencesImpl
,通过Activity
的getSharedPreferences()
方法可以跟踪到它的实现类。 在这里会将创建的xml
文件保存起来,以免多次创建。在创建SharedPreferencesImpl
实例时,还会检测mode
,API 24
后不再支持MODE_WORLD_READABLE
和MODE_WORLD_WRITEABLE
模式。创建后的实例会保存在一个static
的map
里,所以在整个app
的存活过程中,所有实例化后的数据不会被回收,所以不能保存太多的数据,防止OOM
。在多进程的模式下,获取SP
的实例会第二次加载,在后面的分析中可以看出这是一个耗时的IO
操作,所以多进程模式会降低了SP
的效率,而且不可靠。
ContextWrapper.java
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
//这里的mBase是ContextImpl.java
return mBase.getSharedPreferences(name, mode);
}
ContextImpl.java
private ArrayMap<String, File> mSharedPrefsPaths;
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
// 解决以前问题
if (mPackageInfo.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
if (name == null) {
name = "null";
}
}
//按文件名为key,文件为value保存在ArrayMap里
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
file = mSharedPrefsPaths.get(name);
if (file == null) {
//在shared_prefs目录下创建该XML文件
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
@Override
public File getSharedPreferencesPath(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
@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) {
//API 24后不再支持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;
}
}
//多进程模式或API小于11
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
//重新加载一次
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>();
}
final String packageName = getPackageName();
ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
//按包名保存起来
sSharedPrefsCache.put(packageName, packagePrefs);
}
return packagePrefs;
}
四、SP的数据加载和读取
1.加载
SharedPreferencesImpl
初始化后,会开启一个新的线程去加载文件,然后将xml
文件的数据读入map
中。
//初始化
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk();
}
//开始异步加载
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
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 {
//通过XmlUtils将文件读进map里
str = new BufferedInputStream(
new FileInputStream(mFile), 16 * 1024);
map = (Map<String, Object>) XmlUtils.readMapXml(str);
} catch (Exception e) {
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
} catch (Throwable t) {
thrown = t;
}
synchronized (mLock) {
mLoaded = true;
mThrowable = thrown;
try {
//确定没报异常,在对加载的map赋值,记录记载的时间和文件大小
if (thrown == null) {
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtim;//记录记载的时间
mStatSize = stat.st_size;//记录记载的文件大小
} else {
mMap = new HashMap<>();
}
}
} catch (Throwable t) {
mThrowable = t;//记录异常
} finally {//通知其它线程
mLock.notifyAll();
}
}
}
2.读取数据getXXX()
我们一般会在UI
线程读取数据,通过上面分析加载文件是在其它线程,如果文件没加载完(loadFromDisk
没执行结束),在这里的读取操作就能看出,UI
线程就会wait
,如果文件比较大,加载的时间长,就会导致UI
线程长时间等待。
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
private void awaitLoadedLocked() {
if (!mLoaded) {//StrictMode相关检查
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
mLock.wait(); //还没加载完就等起
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {//有异常就在里抛出去
throw new IllegalStateException(mThrowable);
}
}
五、数据保存apply和commit
1.待提交
每次调用edit
方法会返回一个新的EditorImpl
对象,然后在调用putXXX()
方法,会把要待提交的数据放进mModified
的一个map
里。
public Editor commit() {
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
EditorImpl.java
private final Object mEditorLock = new Object();
@GuardedBy("mEditorLock")
private final Map<String, Object> mModified = new HashMap<>();
private boolean mClear = false;
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
2.commit
commit
方法会先将修改的数据提交到内存,放进SP
的map
里。还会在commitToMemory
方法里记录是否真实的发生了改变(key
或value
发生了改变),如果变化了,才会写文件,减少IO
操作。commit
会在主线程一直等待文件写完,然后返回结果。
从这可以看出commit
会在主线程提交数据并等待执行结果,如果数据较大,写入文件时间过于耗时则会阻塞主线程,造成卡顿。
public boolean commit() {
//将数据更新到SP里
MemoryCommitResult mcr = commitToMemory();
//将数据同步到文件里
SharedPreferencesImpl.this.enqueueDiskWrite( mcr, null);
try {
//等文件写完
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
} finally {
}
//通知监听者
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
if (mDiskWritesInFlight > 0) {
mMap = new HashMap<String, Object>(mMap);
}
mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {//监听key变化的监听者
keysModified = new ArrayList<String>();
listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
synchronized (mEditorLock) {
//是否真的发生了变化
boolean changesMade = false;
if (mClear) {//如果标记了清空,则直接把map清空
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
mapToWriteToDisk.clear();
}
mClear = false;
}
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
//value的值为editor对象本身或者null意味着要删除这个key
if (v == this || v == null) {
if (!mapToWriteToDisk.containsKey(k)) {
continue;
}
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) {//这个key变了,记录
keysModified.add(k);
}
}
//把刚才提交在editor的数据清空(已经提交到SP的map里了)
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
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() {
synchronized (mWritingToDiskLock) {
//写入文件
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {//commit没有,apply有
postWriteRunnable.run();
}
}
};
if (isFromSyncCommit) {//commit直接在这执行写入文件
boolean wasEmpty = false;
synchronized (mLock) {
//在commitToMemory方法中该值被加1,所以这为true
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
//直接把writeToDiskRunnable在该线程执行了然后返回
writeToDiskRunnable.run();
return;
}
}
//apply走这
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
3.apply
apply
和commit
不一样的是没有返回值,把数据写到内存后,直接把写文件任务放到其它线程排队执行。
public void apply() {
//将数据更新到SP里
final MemoryCommitResult mcr = commitToMemory();
//这里的一串Runable有点多,主要的作用还是把任务放进QueuedWork里去执行。必要的时候需要阻塞主线程等一等。
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);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
4. writeToFile
把数据写入文件中,在这会检测是否真正需要写入文件到磁盘,commit
会直接写入文件,apply
会检测这是不是最后一次提交,这只需要把最后一次的数据写入磁盘即可。FileUtils.sync(str)
操作确定数据落盘,而不是写到缓冲区中,确保数据不会因为设备断电等原因丢失。
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;
boolean fileExists = mFile.exists();
// 重命名当前文件,以便在下次读取时将其用作备份
if (fileExists) {
boolean needsWrite = false;//是否需要写文件
// 确保发生了变化
if (mDiskStateGeneration < mcr.memoryStateGeneration) {
if (isFromSyncCommit) {//commit方式要写文件
needsWrite = true;
} else {//apply方式只需要写最后一次提交的就行
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();
}
}
//尝试写入文件、删除备份并尽可能原子地返回 true。如果出现任何异常,删除新文件; 下次我们将从备份中恢复
try {
FileOutputStream str = createFileOutputStream(mFile);
if (str == null) {
mcr.setDiskWriteResult(false, false);
return;
}
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
writeTime = System.currentTimeMillis();
//确保文件落盘
FileUtils.sync(str);
fsyncTime = System.currentTimeMillis();
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
try {
final StructStat stat = Os.stat(mFile.getPath());
synchronized (mLock) {//记录文件的时间和大小
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
}
} catch (ErrnoException e) {
// Do nothing
}
// 写入成功,把备份文件删除
mBackupFile.delete();
mDiskStateGeneration = mcr.memoryStateGeneration;
//写入成功,释放锁,返回结果
mcr.setDiskWriteResult(true, true);
long fsyncDuration = fsyncTime - writeTime;
mSyncTimes.add((int) fsyncDuration);
mNumSync++;
return;
} catch (XmlPullParserException e) {
} catch (IOException e) {
}
//写入失败,把文件删除
if (mFile.exists()) {
if (!mFile.delete()) {
Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
}
}
mcr.setDiskWriteResult(false, false);
}
5.startReloadIfChangedUnexpectedly
mode
为MODE_MULTI_PROCESS
情况下,会判断文件是否发生了变化,如果变化了则重新加载一次文件到SP
中。所以多进程下,使用SP
会降低效率,因为SP
里的同步是线程同步,进程间使用也不能保证数据的正确性,所以不能在多进程下使用。官方也已经将MODE_MULTI_PROCESS
标记为Deprecated
。
void startReloadIfChangedUnexpectedly() {
synchronized (mLock) {
if (!hasFileChangedUnexpectedly()) {
return;
}
startLoadFromDisk();
}
}
private boolean hasFileChangedUnexpectedly() {
synchronized (mLock) {
if (mDiskWritesInFlight > 0) {
if (DEBUG) Log.d(TAG, "disk write in flight, not unexpected.");
return false;
}
}
final StructStat stat;
try {
BlockGuard.getThreadPolicy().onReadFromDisk();
stat = Os.stat(mFile.getPath());
} catch (ErrnoException e) {
return true;
}
synchronized (mLock) {//通过文件的更新时间和大小来判断文件是否发生了变化
return !stat.st_mtim.equals(mStatTimestamp) || mStatSize != stat.st_size;
}
}
六、QueuedWork.java
正常的流程,apply
会通过queue
方法将任务添加进一个LinkedList
的sWork
中,然后通过HandlerThread
单线程顺序的执行写入任务。
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 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;
}
}
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();
}
}
}
private static void processPendingWork() {
long startTime = 0;
synchronized (sProcessingWork) {
LinkedList<Runnable> work;
synchronized (sLock) {
work = (LinkedList<Runnable>) sWork.clone();
sWork.clear();
getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
}
//循环执行任务
if (work.size() > 0) {
for (Runnable w : work) {
w.run();
}
}
}
}
另外的一种情况,在QueuedWork
有个waitToFinish
方法,它在ActivityThread
的handleStopService
和handleStopActivity
等方法中有调用,调用处主要有Activity
的onPause
,BroadcastReceiver
的onReceive
,Service
的stop
等情况下。通过代码可以看出,在这些情况下,如果写文件任务还有没完成的,直接把写任务放在主线程执行了,取消其它线程执行该任务。这样可以保证写文件任务完成,防止数据丢失。在API 8.0
以前waitToFinish
方式实现不一样,以前直接阻塞主线程等待任务完成。
public static void waitToFinish() {
long startTime = System.currentTimeMillis();
boolean hadMessages = false;
Handler handler = getHandler();
synchronized (sLock) {
if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
// 下面要直接执行,所以在这删除了
handler.removeMessages(QueuedWorkHandler.MSG_RUN);
}
// 不能延迟等了
sCanDelay = false;
}
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
try {
//直接把任务拿出来执行
processPendingWork();
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
try {
while (true) {//把apply添加的清尾工作finisher执行了
Runnable finisher;
synchronized (sLock) {
finisher = sFinishers.poll();
}
if (finisher == null) {
break;
}
finisher.run();
}
} finally {
sCanDelay = true;
}
synchronized (sLock) {
long waitTime = System.currentTimeMillis() - startTime;
if (waitTime > 0 || hadMessages) {
mWaitTimes.add(Long.valueOf(waitTime).intValue());
mNumWaits++;
if (DEBUG || mNumWaits % 1024 == 0 || waitTime > MAX_WAIT_TIME_MILLIS) {
mWaitTimes.log(LOG_TAG, "waited: ");
}
}
}
}
七、总结
综上,
-
commit
和apply
一个有返回值,一个没有,一个同步提交,一个异步提交。 - 一个文件里的key过多会导致文件大,加载变慢,写入变慢。读取数据时没加载完就会阻塞主线程,造成卡顿,严重就会
ANR
,可以将key
按功能拆分文件。SP
内存不会回收,所以文件多了也不行,本身定位就是小数据保存。 - 不能在多进程使用
- 不需要结果时,尽量使用
apply
,commit
会在主线程写文件。但是apply
也不一定可靠,在waitToFinish
里叙述了。 - 每个版本可能实现不一样,表现不一样,加大了
bug
修复难度,所以类似放进Jetpack
里单独可升级感觉有必要。
本文参考:
全面剖析SharedPreferences
Android 之不要滥用 SharedPreferences(下)
SharedPreferences ANR问题分析和解决 & Android 8.0的优化
剖析 SharedPreference apply 引起的 ANR 问题