前言
前一天被朋友问到下面几个问题 :
1、apply和commit有何区别,是否会堵塞主线程,推荐用哪一种?
2、是否是进程安全的,为什么?
3、要做到进程安全,该如何设计?
源码环境 :API 28 Andorid 9.0
目录
一、加载、初始化
首先看SharedPreferences的加载
ContextImpl.java
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
if (mPackageInfo.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
if (name == null) {
name = "null";
}
}
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
file = mSharedPrefsPaths.get(name);
if (file == null) {
file = getSharedPreferencesPath(name); // 在shared_prefs 下生成 name.xml文件
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode); //1
}
这里主要是映射文件名和文件到mSharedPreferences中,接下来看下 1 处注释方法
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();//1
sp = cache.get(file);
if (sp == null) {
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;
}
}
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
上述代码注释 1 处 是从 sSharedPrefsCache中根据包名获取ArrayMap<File,SharedPreferencesImpl>,然后再根据文件获取对应的SharedPreferencesImpl实例,没有的话,就会先检查模式(从 Android N 开始, 不再支持 MODE_WORLD_READABLE & MODE_WORLD_WRITEABLE),然后根据File和模式创建一个SharedPreferencesImpl实例。
SharedPreferencesImpl.java
//1\. 构造方法
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk();
}
//2.
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
//3
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) {
return;
}
// 4\. 如果备份文件.bak存在 则先删除原文件 ,然后备份文件重命名 为原文件名
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);
......
} finally {
mLock.notifyAll();
}
}
}
可以看到SharedPreferencesImpl构造函数开启了线程去解析xml文件,详细看 注释 3 处 方法,把解析xml文内容用键值对存放在map内存中。
大概流程如下,主要步骤是开启线程去读磁盘文件,解析到map中。
二、commit 和 apply方法
1、commit方法
commit和apply都是通过SharedPreferences的EditorImpl类实现的,我们来看一下commit实现原理
@Override
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
MemoryCommitResult mcr = commitToMemory(); //注释 1
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */); // 注释 2
try {
// 注释 2 这里内部调用了CountDownLatch的awiat等待方法,只有在写完文件后才会放行
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
} finally {
......
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
依次跟入注释 1 、2 处代码,详细解释见代码后注释
commitToMemory()
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; // 1.将加载时的map引用 给 mapToWriteToDisk
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
keysModified = new ArrayList<String>();
listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
synchronized (mEditorLock) { //2.持有mEditorLock锁,put值时需等待
boolean changesMade = false;
if (mClear) {
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
mapToWriteToDisk.clear();
}
mClear = false;
}
for (Map.Entry<String, Object> e : mModified.entrySet()) { // 3.每次put值,都是放入mModified Map中
String k = e.getKey();
Object v = e.getValue();
if (v == this || v == null) { //4\. value == this(Editor) 或者 value == null 当前数据不符合要求
if (!mapToWriteToDisk.containsKey(k)) {
continue;
}
mapToWriteToDisk.remove(k); //5.移除 value值有问题的
} else {
//6.如果有这个key ,如果value相同,则不加入 mapToWriteToDisk 中,没有的话加入
if (mapToWriteToDisk.containsKey(k)) {
Object existingValue = mapToWriteToDisk.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mapToWriteToDisk.put(k, v);
}
//7\. for循环走了 说明 mapToWriteToDisk 被修改了, 加入或者 删除了值
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
//8.存放要提交的数据的 mModified 清除
mModified.clear();
if (changesMade) {
//9.当前内存状态值 +1
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
//10.构建 MemoryCommitResult对象
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
enqueueDiskWrite(mcr,null)
注意这里第二个参数是传的null
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
//1\. 同步还是异步的标志 这里commit传递过来的 postWriteRunnable是 null,所以isFromSysnCommit 为true,代表同步操作
final boolean isFromSyncCommit = (postWriteRunnable == null);
// 2\. 构建Runnable 主要是写文件 待会具体看一下
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
// ==== 稍后分析 =====
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
//2.commit 会执行到这里
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
//3\. 正在执行的写入磁盘操作的数量 == 1
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
//4.加入到sWork队列中
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();
synchronized (sLock) {
//1.加入到 LinkedList sWork中
sWork.add(work);
//1\. commit 提交的话 shouldDelay 为 false
if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
上面这个handler 是 QueuedWorkHandler类型的(根据HandlerThread创建的Handler),处理逻辑在他的handleMessage中,该方法只有 processPendingWork()一个方法
private static void processPendingWork() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
synchronized (sProcessingWork) {
LinkedList<Runnable> work;
synchronized (sLock) {
//1.浅 复制 到 work 集合中
work = (LinkedList<Runnable>) sWork.clone();
sWork.clear();
// Remove all msg-s as all work will be processed now
getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
}
if (work.size() > 0) {
//2\. for循环执行 run方法
for (Runnable w : work) {
w.run();
}
if (DEBUG) {
Log.d(LOG_TAG, "processing " + work.size() + " items took " +
+(System.currentTimeMillis() - startTime) + " ms");
}
}
}
}
到此commit分析基本结束了,还剩下一个写文件操作,就是构建Runnable时 run方法内调用的 writeToFile(mcr, isFromSyncCommit) 方法
@GuardedBy("mWritingToDiskLock")
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();
......
//11.原文件是否存在
if (fileExists) {
boolean needsWrite = false;
// 2.本次是否需要 提交 写入文件 判断
if (mDiskStateGeneration < mcr.memoryStateGeneration) {
if (isFromSyncCommit) {
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) {
//3.不需要写入 直接返回
mcr.setDiskWriteResult(false, true);
return;
}
//4\. 刚开始SP加载时,根据原文件创建了备份文件,然后删除了原文件。这里判断备份文件是否存在
boolean backupFileExists = mBackupFile.exists();
......
//5\. 备份文件不存在
if (!backupFileExists) {
if (!mFile.renameTo(mBackupFile)) {
Log.e(TAG, "Couldn't rename file " + mFile
+ " to backup file " + mBackupFile);
mcr.setDiskWriteResult(false, false);
return;
}
} else {
//6\. 备份文件存在 则删除原文件
mFile.delete();
}
}
//7\. 重新把内容写到原文件中,设置Mode权限,写失败 尝试删除原文件
// 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.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;
// 7.1 写成功后,CountDownLatch 放行,提交返回结果,才正式结束
mcr.setDiskWriteResult(true, true);
......
long fsyncDuration = fsyncTime - writeTime;
mSyncTimes.add((int) fsyncDuration);
mNumSync++;
if (DEBUG || mNumSync % 1024 == 0 || fsyncDuration > MAX_FSYNC_DURATION_MILLIS) {
mSyncTimes.log(TAG, "Time required to fsync " + mFile + ": ");
}
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);
}
总结一下,commit操作,首先它会构建MemoryCommitResult对象,把编辑的结果同步到内存中,然后将结果写入到磁盘文件中。在写文件的过程中,会利用CountDownLatch阻塞等待,直到写文件成功后才会notify,成功写入文件后会将备份文件删除,同一个SP实例,下次再提交数据时,会将原文件重命名备份文件名。如果写入失败,会将原文件删除。由此可见,数据commit都会重新写入整个文件数据。(备份文件作用是用来给下次恢复数据使用,可以见SP构造函数实例)
2、apply方法
@Override
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
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);
}
和commit差不多,apply方法没有。返回值我们直接看 enqueueDiskWrite 方法,这次第二个参数是postWriteRunnable ,不为空。从之前Commit分析可以知道,就是利用QueuedWorkHandler进行了延迟发送,默认100ms,防止频繁写入,并且QueuedWorkHandler是通过HandlerThread创建的,其它区别可以忽略不计。
这里再分析一下上述代码中的 QueuedWork.addFinisher(awaitCommit) 方法
public static void addFinisher(Runnable finisher) {
synchronized (sLock) {
sFinishers.add(finisher);
}
}
它会将该awaitCommit放到sFinishers中,执行后才会remove掉。我们再来看一段代码
/**
* Trigger queued work to be processed immediately. The queued work is processed on a separate
* thread asynchronous. While doing that run and process all finishers on this thread. The
* finishers can be implemented in a way to check weather the queued work is finished.
*
* Is called from the Activity base class's onPause(), after BroadcastReceiver's onReceive,
* after Service command handling, etc. (so async work is never lost)
*/
public static void waitToFinish() {
......
try {
while (true) {
Runnable finisher;
synchronized (sLock) {
finisher = sFinishers.poll();
}
if (finisher == null) {
break;
}
finisher.run();
}
} finally {
sCanDelay = true;
}
.....
}
看这段源码的注释就知道,框层架确保在切换状态之前完成使用apply()方法 正在执行磁盘写入的动作会在Activiy的 onPause()、BroadcastReceiver的onReceive()以及Service的onStartCommand()方法之前调用waitToFinish方法,从这一点也可以看出时会阻塞线程的。
3、apply和commit对比
个人觉得主要区别在于apply方式提交的时候,会有一个消息延迟100ms发送,避免频繁的磁盘写入,而commit提交时,是直接利用Handler发送消息的。注意这里都是同一个Handler,都是在不同调用线程的子线程中执行的写入文件操作。
4、关于get、put数据
我们直接看getString和putString方法,其它类似
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
这里会去获取mLock的锁,获取不到就等待,只有在解析完xml后,该锁才会被释放,或者跟写相关的操作都会排队来获取或者释放该锁,这样保证了数据的正确性。putString也类似,只不过不是同一个对象的锁。
@Override
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
三、总结
1、apply和commit有何区别,是否会堵塞主线程,推荐用哪一种?
首先看一下commit和apply方式提交数据,apply提交的时候,会有一个消息延迟100ms发送,避免频繁的磁盘写入,而commit提交时,是直接利用Handler发送消息的。注意这里都是同一个Handler,都是在不同调用线程的子线程中执行的写入文件操作。
但是这两个方法其实都是阻塞线程的,提交数据时都涉及调用CountDownLatch的await,文件写入成功后才会调用downLatch方法,所以这是阻塞现成的。并且在apply方法中有一个
推荐使用apply,每次写数据都设计重新将数据写入文件,apply具有100ms的延迟避免频繁写入。
2、是否是进程安全的,为什么?
SharedPreferences是线程安全的,这个毋庸置疑,你看方法内大量的synchronized就是用来保障数据正确性的。
但它不是进程安全的,同一个文件,A进程正在读取,B进程正在写,B进程加载SP时会将备份文件重命名为原文件名,A进程读取,读取到的数据只能是之前原文件写入到内存中的文件,B进程写入的内容其他在操作的进程也无法获取。
3、要做到进程安全,该如何设计?
对于需要进程安全版的SP,首选MMKV
对于MMKV和SharedPreferences的对比和MMKV总结 Android存储优化
当然还有其它方案,都是可以说都没有MMKV好(接入简单),例如
-
使用ContentProvider包裹SP,进程访问SP,通过ContentProvider来访问。
-
使用广播,实现状态同步,但是即时性不好
-
Socket ,每个进程都需要维护一个套接字,数据安全、使用难度较高。
-
文件+文件锁形式