前言
最近一段时间一直在Review《祖传代码》, 用AndroidStudio Inspect Code扫描时候发现一段Warning 。出于好奇,扒一扒源码(源码是最好的老师) 。从源码分析一下commit()和apply()到底有什么区别。下文是来自编译器提示warning⚠️ 内容。
Consider using 'apply()' instead; 'commit' writes its data to persistent storage immediately, whereas 'apply' will handle it in the background
- 翻译成中文大概意思:
开发者应考虑使用apply(),来替代commit()。commit(同步)会将数据写入持久化存储,apply(异步)会在后台处理。
commit方法源码注释
/**
* Commit your preferences changes back from this Editor to the
* {@link SharedPreferences} object it is editing. This atomically
* performs the requested modifications, replacing whatever is currently
* in the SharedPreferences.
*
* Note that when two editors are modifying preferences at the same
* time, the last one to call commit wins.
*
*
If you don't care about the return value and you're
* using this from your application's main thread, consider
* using {@link #apply} instead.
*
* @return Returns true if the new values were successfully written
* to persistent storage.
*/
boolean commit();
-
针对commit()源码中的注释进行一下解读(英文水平高,直接看就好):
多并发commit时,需要等待正在处理commit数据更新到磁盘中,才会继续执行,相比apply效率较低。
commit()是有返回值,布尔类型,返回是否成功写入数据,不考虑结果可以使用apply()。
写入数据是同步操作
apply方法源码注释
/**
* Commit your preferences changes back from this Editor to the
* {@link SharedPreferences} object it is editing. This atomically
* performs the requested modifications, replacing whatever is currently
* in the SharedPreferences.
*
* Note that when two editors are modifying preferences at the same
* time, the last one to call apply wins.
*
*
Unlike {@link #commit}, which writes its preferences out
* to persistent storage synchronously, {@link #apply}
* commits its changes to the in-memory
* {@link SharedPreferences} immediately but starts an
* asynchronous commit to disk and you won't be notified of
* any failures. If another editor on this
* {@link SharedPreferences} does a regular {@link #commit}
* while a {@link #apply} is still outstanding, the
* {@link #commit} will block until all async commits are
* completed as well as the commit itself.
*
*
As {@link SharedPreferences} instances are singletons within
* a process, it's safe to replace any instance of {@link #commit} with
* {@link #apply} if you were already ignoring the return value.
*
*
You don't need to worry about Android component
* lifecycles and their interaction with apply()
* writing to disk. The framework makes sure in-flight disk
* writes from apply()
complete before switching
* states.
*
*
The SharedPreferences.Editor interface
* isn't expected to be implemented directly. However, if you
* previously did implement it and are now getting errors
* about missing apply()
, you can simply call
* {@link #commit} from apply()
.
*/
void apply();
-
针对apply()源码中的注释进行一下解读(英文水平高,直接看就好):
apply写入过程中会阻塞同一个SharedPreferences对象其它写入操作。
apply()是void没有任何返回值,
写入数据是异步操作,与commit方式不同,apply以原子形式将数据提交到内存中,apply()调用的函数直接覆盖之前内存数据,然后异步提交到磁盘上,效率上相比commit要高。
原子性
什么是原子性 ?
这个思想体现在多线程并发操作里,即一个操作或者多个操作,要么全部执行,执行过程不会被任何因素打断,要么全部不执行。
经典的转账问题:
比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。
试想一下,如果这2个操作不具备原子性,会造成什么样的后果。
假如从账户A减去1000元之后,操作突然中止。然后又从B取出了500元,取出500元之后,再执行 往账户B加上1000元 的操作。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。
SharedPreferences 源码流程分析
本文所用的源码为安卓30, AndroidStudio中SDK Platforms里下载。
从源码中可知 SharedPreferences是个接口,需要找一下SharedPreferences具体实现类。SharedPreferencesImpl 类是SharedPreferences具体实现,从这个方法里能找到commit和apply的具体逻辑。
commit()
简单说一下这块(注释很细可参考)。将Editor对象设置的数据存储到内存中,存储时主线程处于阻塞状态,等到写入物理磁盘有结果(成功或者失败)后,解除阻塞状态。同步回调结果和写入物理磁盘结果。
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
//将编辑(Editor)的数据,提交到内存
MemoryCommitResult mcr = commitToMemory();
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");
}
}
//就是对onSharedPreferenceChanged结果回调。
notifyListeners(mcr);
//返回数据是否写入成功
return mcr.writeToDiskResult;
}
commitToMemory方法
We optimistically don't make a deep copy until a memory commit comes in when we're already writing to disk.
翻译:在数据写入物理媒介前,代码不会直接进行复制,先将数据提交到内存。
该方法的主要功能:
把EditorImpl数据更新到SharedPreferencesImpl,这样说可能有点不好理解,在通俗一点说commitToMemory()将Editor编辑的数据同步到内存中。代码中注释比价详细,捋一捋主要的东西。
- if (mDiskWritesInFlight > 0) {} 说明:HashMap本身不是一个线程安全的集合,因此会对全局的mMap进行一次拷贝,让其他线程可以正常的查询数据
- mapToWriteToDisk将要写入物理磁盘的数据。mMap是全局变量,用来记录之前存过的数据,第一次执行commitToMemory()时候mapToWriteToDisk和mMap都是0,等到writeToFile()执行完后,会在指定路径生成xxx.xml, 再次调用,mMap就是从存储xxx.xml数据中解析存过的数据。
- mModified.entrySet()是来自Editor内部实现(EditorImpl),存储Editor put过的key和value,然后mapToWriteToDisk接收来自Editor(编辑)数据。mModified清空掉数据,为下一次存储数据做准备。(感觉Editor就是一个载体,临时文件中转)
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
boolean keysCleared = false;
List keysModified = null;
Set listeners = null;
Map mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
//如果mDiskWritesInFlight>0,说明其他线程在进行写入操作。
if (mDiskWritesInFlight > 0) {
//复制mMap,对新数据进行操作
mMap = new HashMap(mMap);
}
//正常情况下会走到这里赋值,
mapToWriteToDisk = mMap;
//默认值是0 ,自增+1
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
//是否设置了OnSharedPreferenceChangeListener监听
if (hasListeners) {
keysModified = new ArrayList();
listeners = new HashSet(mListeners.keySet());
}
synchronized (mEditorLock) {
//内存数据有没有修改
boolean changesMade = false;
//如果mClear等于true,说明调用过Editor.clear()
if (mClear) {
//如果mClear为true,直接清空mapToWriteToDisk(mMap)数据
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
mapToWriteToDisk.clear();
}
keysCleared = true;
mClear = false;
}
// mModified在Editor.putxxx操作已经存放数据了
for (Map.Entry e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
//EditorImpl存入的value 值等于自身对象或者null,移除对应的key
if (v == this || v == null) {
//如果之前就不存在该key, 跳过本次操作,没有任何意义。
if (!mapToWriteToDisk.containsKey(k)) {
continue;
}
mapToWriteToDisk.remove(k);
} else {
if (mapToWriteToDisk.containsKey(k)) {
Object existingValue = mapToWriteToDisk.get(k);
//判断value有没有做修改,没有做过修改,跳过本次操作
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
//将新添加的k,v添加到磁盘中,准备写入物理媒介
mapToWriteToDisk.put(k, v);
}
changesMade = true;
//设置了OnSharedPreferenceChangeListener监听
if (hasListeners) {
//记录发生改变的key
keysModified.add(k);
}
}
//清空EditorImpl中的mModified数据.
mModified.clear();
//数据已经改变,内存变换次数自增
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
//将MemoryCommitResult对象返回
return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
listeners, mapToWriteToDisk);
}
MemoryCommitResult
MemoryCommitResult类是SharedPreferencesImpl类的静态内部类。用来保存对内存整理的结果。 这部分没什么好说的,不贴代码了。略微说一下重要的。
CountDownLatch一种同步类,允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。
CountDownLatch默认值是1 ,调用await()线程被挂起,代表着阻塞(等待)。调用setDiskWriteResult()计数器会-1 ,值变成0,代表解除阻塞。
enqueueDiskWrite
将已提交到内存的结果排入队列以写入磁盘。会按照它们入队的顺序一次一个地写入磁盘。
enqueueDiskWrite方法可分,Runnable对象为null(apply),和不为null两种情况(commit)。
等于null(commit),isFromSyncCommit为true。然后调用writeToFile,mDiskWritesInFlight计数自减。 postWriteRunnable.run不执行
不等于null(apply), QueuedWork.queue()。然后调用writeToFile,mDiskWritesInFlightj计数自减。 postWriteRunnable.run 执行
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
// 区分同步写入物理媒介,还是异步写入物理媒介
//commit()中传的是null,可以将isFromSyncCommit变更为true;
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
//真正执行写入文件的操作
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
// postWriteRunnable不为空,针对的是apply()
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
//同步写入物理媒介
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
//执行 commitToMemory()过程会加1,上面有说过,wasEmpty为true
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
// 执行 上面的 writeToDiskRunnable()
writeToDiskRunnable.run();
return;
}
}
//异步写入
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
writeToFile
这部分就是将内存中的数据写入到物理磁盘中
第一次执行,mFile文件并不存在(还没有创建),直接走到FileOutputStream,将内存中的数据写入到具体物理路径下xxx.xml,这部分方法流程结束(没全细说,有注释方便理解)。
第二次要调用writeToFile(),fileExists已经存在。mDiskStateGeneration < mcr.memoryStateGeneration 不成立说明数据相同,没有任何改变。setDiskWriteResult把结果回调过去,解除阻塞状态,return 结束不往下执行。反之有新的数据该走到mBackupFile.exists(),然后走到FileOutputStream,将新的数据全量写入到xxx.xml
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();
}
//文件是否存在,mFile路径(我的测试机)/data/user/0/packageName/shared_prefs/xxx.xml,
//xxx为 getSharedPreferences("xxx",Context.MODE_PRIVATE)
boolean fileExists = mFile.exists();
if (DEBUG) {
existsTime = System.currentTimeMillis();
// Might not be set, hence init them to a default value
backupExistsTime = existsTime;
}
// xml存在,重命名当前文件,以便下次读取时用作备份
if (fileExists) {
boolean needsWrite = false;
//当磁盘状态早于本次提交时,才需要写入
if (mDiskStateGeneration < mcr.memoryStateGeneration) {
//同步写入commit()
if (isFromSyncCommit) {
needsWrite = true;
} else {
//异步写入apply()
synchronized (mLock) {
//如果是最新的数据,需要写入物理媒介。
if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
needsWrite = true;
}
}
}
}
//不需要写入,设置写入结果。
if (!needsWrite) {
mcr.setDiskWriteResult(false, true);
return;
}
//备份文件是否存在
boolean backupFileExists = mBackupFile.exists();
if (DEBUG) {
backupExistsTime = System.currentTimeMillis();
}
// 不存在备份文件
if (!backupFileExists) {
//将mFile重命名为备份文件(数据做一下备份)
if (!mFile.renameTo(mBackupFile)) {
Log.e(TAG, "Couldn't rename file " + mFile
+ " to backup file " + mBackupFile);
mcr.setDiskWriteResult(false, false);
return;
}
} else {
mFile.delete(); //删除mFile文件
}
}
//写入文件,删除备份,并返回true,尽可能保持原子性。
//如果出现异常,删除新文件,下次从备份文件中恢复数据
try {
//创建文件输出流
FileOutputStream str = createFileOutputStream(mFile);
if (DEBUG) {
outputStreamCreateTime = System.currentTimeMillis();
}
if (str == null) {
mcr.setDiskWriteResult(false, false);
return;
}
//将内存中的数据写入 到物理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();
}
//有备份文件删除
mBackupFile.delete();
if (DEBUG) {
deleteTime = System.currentTimeMillis();
}
mDiskStateGeneration = mcr.memoryStateGeneration;
//设置物理磁盘写入结果,到这里返回的肯定是写入成功。
mcr.setDiskWriteResult(true, true);
if (DEBUG) {
Log.d(TAG, "write: " + (existsTime - startTime) + "/"
+ (backupExistsTime - startTime) + "/"
+ (outputStreamCreateTime - startTime) + "/"
+ (writeTime - startTime) + "/"
+ (fsyncTime - startTime) + "/"
+ (setPermTime - startTime) + "/"
+ (fstatTime - startTime) + "/"
+ (deleteTime - startTime));
}
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);
}
// 清理没有成功写入的文件
if (mFile.exists()) {
//无法删除的文件
if (!mFile.delete()) {
Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
}
}
mcr.setDiskWriteResult(false, false);
}
apply()
commitToMemory在上面已经说过,不再复述。apply 方法,创建了一个 awaitCommit 的 Runnable,然后加入到 QueuedWork 中,awaitCommit 中包含了一个等待锁,需要在其它地方释放。
通过 SharedPreferencesImpl.this.enqueueDiskWrite 创建了一个任务来执行真正的 SP 持久化。
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) {
}
if (DEBUG && mcr.wasWritten) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " applied after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
};
// QueuedWork.addFinisher:添加一个异步处理工作,用于等待。
//实际上并没有开始运行,只是为了执行异步工作调用着准备临时集。
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
//将awaitCommit对象从QueuedWork移除
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);
}
QueuedWork.queue
/**
* Queue a work-runnable for processing asynchronously.
* @param work The new runnable to process
* @param shouldDelay If the message should be delayed
*/
翻译 :
QueuedWork :异步处理可运行的工作排入队列(系统)。
work : 新的可运行文件
shouldDelay : 是否需要延迟。
public static void queue(Runnable work, boolean shouldDelay) {
//在单独的线程创建处理程序
Handler handler = getHandler();
synchronized (sLock) {
//添加任务队列
sWork.add(work);
if (shouldDelay && sCanDelay) {
//延迟 0.1s发送消息
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
//实时发消息
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
getHandler()
这部分内容就是开辟一个单独的线程执行工作任务。sHandler为QueuedWorkHandler对象。当调用sHandler发送消息,消息会在QueuedWorkHandler类的handlerMessage方法进行处理。 收到消息后,调用processPendingWork方法进行处理。
private static Handler getHandler() {
synchronized (sLock) {
//第一次调用的默认 null, if条件成立。
//不等null直接返回sHandler对象
if (sHandler == null) {
//创建一个名为queued-work-looper实例对象
HandlerThread handlerThread = new HandlerThread("queued-work-looper",
Process.THREAD_PRIORITY_FOREGROUND);
//开启线程
handlerThread.start();
//根据HandlerThread线程的looper创建QueuedWorkHandler对象
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();
}
}
}
processPendingWork
处理待处理的工作,执行写入磁盘任务,至此apply流程走完 。
private static void processPendingWork() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
synchronized (sProcessingWork) {
LinkedList work;
synchronized (sLock) {
//将sWork待执行队列复制 work中
work = (LinkedList) sWork.clone();
//清空全局变量
sWork.clear();
// 移除消息队列中的消息
getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
}
//如果执行任务数量大于0
if (work.size() > 0) {
for (Runnable w : work) {
//执行写入磁盘任务
w.run();
}
if (DEBUG) {
Log.d(LOG_TAG, "processing " + work.size() + " items took " +
+(System.currentTimeMillis() - startTime) + " ms");
}
}
}
}
结论
-
commit
建议在子线程中调用,commit()在主线程写入文件,会造成UI卡顿。在多个并发 操作commit的时候,它们会等待正在处理的commit保存到磁盘后,有结果在解除阻塞,从而降低了效率。
-
apply
建议在主线程中调用。执行效率相对于commit是高的,官方也是非常建议,毕竟从效率上讲异步总是比同步要快。But apply()在子线程写入文件,也有可能卡UI。因为主线程有调用的代码,需要等写入任务执行完了才会继续往下执行
-
对比一下两者回调的时机
commit是在内存和硬盘操作均结束时回调
apply是内存操作结束时就进行回调
小建议
官方给 SharedPreferences 定位就是轻量级的数据存储类,存一些简单的数据没问题,存一些大数据暴露出的问题和隐患还是很多,例如明显的卡顿或ANR。
参考文章
- Java并发-理论基础 (文中原子理论出处)