背景:对于Android开发者而言,经常需要在开发中使用SharedPreferences
做一些数据的持久化。一般场景是一些标记或者配置数据。
而在针对一些场景到底该使用
commit()
和apply()
中的哪一个来持久化数据的时候,只知道commit
是以同步的方式在写数据,可能会造成主线成的卡顿,apply()
是异步的写数据,不会造成卡顿。那么实际情况是这样吗?接下来,我们分析一下源码。
【以下SharedPreferences简称SP】
跟踪源代码:
-
一般我们使用SharedPreferences是如下使用:
SharedPreferences sharedPreferences = context.getSharedPreferences("sandbox", Activity.MODE_PRIVATE); //读数据 sharedPreferences.getString(key,defaultValue); sharedPreferences.getInt(key,defaultValue); //写数据 SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putString(key,value) .putInt(key,value) .commit()//或者apply()
-
我们追踪一下数据,
ContextImpl.java
中@Override 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. //如果name为空,则默认为null.xml if (mPackageInfo.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.KITKAT) { if (name == null) { name = "null"; } } //从一个map中根据name来获取自己的SP,这个map存放不同业务的SP文件(xml文件) File file; synchronized (ContextImpl.class) { if (mSharedPrefsPaths == null) { mSharedPrefsPaths = new ArrayMap<>(); } file = mSharedPrefsPaths.get(name); if (file == null) { file = getSharedPreferencesPath(name); mSharedPrefsPaths.put(name, file); } } return getSharedPreferences(file, mode); } @Override public SharedPreferences getSharedPreferences(File file, int mode) { SharedPreferencesImpl sp; //cache中存放了不同业务(不同name的SP实例对象) synchronized (ContextImpl.class) { final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked(); 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) { // 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; }
看以上代码可知,
mSharedPrefsPaths
这个map
里面有各个不同文件名的xml文件(SharedPreferences)
。根据name可以从缓存cache中拿到自己的需要的那个SharedPreferencesImpl
。 -
我们接着看
SharedPreferencesImpl.java
//构造方法 SharedPreferencesImpl(File file, int mode) { mFile = file; mBackupFile = makeBackupFile(file); mMode = mode; mLoaded = false; mMap = null; mThrowable = null; //开始从磁盘文件中load数据到内存 startLoadFromDisk(); } //load数据,子线程工作 private void startLoadFromDisk() { synchronized (mLock) { mLoaded = false; } new Thread("SharedPreferencesImpl-load") { public void run() { loadFromDisk(); } }.start(); } //load数据 private void loadFromDisk() { ...省略 try { stat = Os.stat(mFile.getPath()); if (mFile.canRead()) { BufferedInputStream str = null; try { str = new BufferedInputStream( new FileInputStream(mFile), 16 * 1024); //解析xml文件,将key-value放进map 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) { //将map赋值给成员变了mMap mMap = map; mStatTimestamp = stat.st_mtim; mStatSize = stat.st_size; } else { 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(); } } } //读取数据 @Override @Nullable public String getString(String key, @Nullable String defValue) { synchronized (mLock) { awaitLoadedLocked(); //从成员变量mMap中读取 String v = (String)mMap.get(key); return v != null ? v : defValue; } }
总结读取步骤:
getSpp---》 new SharedPreferencesImpl(file, mode)---》loadFormDisk()解析xml的key-value给mMap---》从mMap中读取数据
- 再看写数据:
public final class EditorImpl implements 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) {
synchronized (mEditorLock) {
//往map mModified中写数据,所以SharedPreferences的put操作是先将数据写到内存,然后在写到xml文件
mModified.put(key, value);
return this;
}
}
....省略
@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() {
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(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);
}
// Returns true if any changes were made
//put的数据同步到内存中
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
Map<String, Object> mapToWriteToDisk;
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 = new HashMap<String, Object>(mMap);
}
//将mMap的引用传递给mapToWriteToDisk,所以二者的数据会同步变化
mapToWriteToDisk = mMap;
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) {
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
mapToWriteToDisk.clear();
}
mClear = false;
}
//便利mModified这个map的数据,然后将数据放进mapToWriteToDisk
//从而将数据也同步到了成员变量mMap中,内存中的数据会立即变化。
//同步和异步都是一样的,会立即将数据写到内存,二者不同的是将数据写xml文件这步
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;
}
mapToWriteToDisk.remove(k);
} else {
if (mapToWriteToDisk.containsKey(k)) {
Object existingValue = mapToWriteToDisk.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
//将modified的数据塞进map中
mapToWriteToDisk.put(k, v);
}
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
//同步方式写数据
/**
*1、先将put的数据同步到内存中(这步是通过commitToMemmory实现的)
*2、再将数据写到文件中
*
**/
@Override
public boolean commit() {
...
//1. 将数据先同步内存
MemoryCommitResult mcr = commitToMemory();
//2.同步写。这里我们去看看enqueueDiskwrite方法
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
//countDownlatch等待写完,才能往下走
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;
}
/**
*这个注释的主要意思:
*已经入队的任务,会按顺序写进文件中。
*如果postWriteRunnable为空,则是来自commit()
*否则是来自apply()
*commit()方式调用允许在主线成上写文件,有点是可以避免开辟线程资源
**/
/**
* Enqueue an already-committed-to-memory result to be written
* to disk.
*
* They will be written to disk one-at-a-time in the order
* that they're enqueued.
*
* @param postWriteRunnable if non-null, we're being called
* from apply() and this is the runnable to run after
* the write proceeds. if null (from a regular commit()),
* then we're allowed to do this disk write on the main
* thread (which in addition to reducing allocations and
* creating a background thread, this has the advantage that
* we catch them in userdebug StrictMode reports to convert
* them where possible to apply() ...)
*/
//入队写文件任务
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
//是否来自commit()
final boolean isFromSyncCommit = (postWriteRunnable == null);
//封装一个写文件的任务Runnable,commit()则直接调用run()
//如果是apply()则将runnable再post出去
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
//来自commit的嗲用,则直接调用run()
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
//来自apply()的调用,则将任务post出去,异步执行
//接下来,我们再看一下QueueWork.queue()
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
//-------这部分是QueuedWork.java代码------------------
//执行异步任务,apply()--->QueuedWork.queue(writeToDiskRunnable, true)
//所以,apply的任务会延时执行,延时为
//private static final long DELAY = 100;即100ms
/**
* Queue a work-runnable for processing asynchronously.
*
* @param work The new runnable to process
* @param shouldDelay If the message should be delayed
*/
@UnsupportedAppUsage
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);
}
}
}
//-------这部分是QueuedWork.java代码-----------------------
综上,可以得到结论:
- SP在对象初始化的时候,会将数据都从磁盘文件load进内存。且应该当作一个单例来使用,否则每次get都会重复去loadFromDisk操作。
- commit()和apply()都会先将数据立即同步到内存中,然后将数据写到xml文件中
- 二者在第一步:将修改的数据同步到内存都是commitToMemory()操作,二者是一样的,没有区别。
- 二者在第二步:将数据写到xml文件中是不一样的。commit()会在主线成立即将数据落地。而apply()是抛出一个延时100ms的异步任务去写文件,并不是及时的。
- 所以,如果是比较重要的数据,应该用commit,如果是不太重要的数据,则换apply()
- 因为SP是用来存储一些轻量数据,而且数据都会load进内存,所以,不应该用SP来存大数据,否则会出现OOM。
- SP是可以注册Listener来监听变换的
registerOnSharedPreferenceChangeListener