前言
SharedPreferences是Android中经常使用的功能之一,不过很多时候对它内部的实现并没有太多关注,比如合适加载它的本地缓存到内存中?有没有线程同步的问题。这篇文章就几个平时不太关注的点来简单说一说
由于SharedPreferences是接口,另外context是抽象类,所以要想看源码,需要在它们的实现类中,在android-sdk\sources\android-xxx\android\app下,分别是SharedPreferencesImpl.java和ContextImpl.java。
线程问题
SharedPreferences是线程同步的,这点在SharedPreferencesImpl下对应的get、put等函数中都可以看到,第一步就是请求同步锁,如:
java
@Override
public boolean getBoolean(String key, boolean defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Boolean v = (Boolean)mMap.get(key);
return v != null ? v : defValue;
}
}
@Override
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
commit和apply
老生常谈了,commit会立刻写入内存和文件,所以有返回值,是否成功。而apply则先写入内存,然后异步写入文件,所以没有返回值。
如果不关心返回值,则最好使用apply。因为可以减少io操作,比如同时大量的修改,apply会先将修改封装并放入一个队列,然后通过handler发送一个消息,如果消息执行,则将队列中的事件处理。因为handler的消息并不一定及时执行,比如前面消息很多,或者ui操作比较多,所以会延迟集中处理,这样也节省了系统资源。
不论commit还是apply,都会执行enqueueDiskWrite,源码如下:
java
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) {
postWriteRunnable.run();
}
}
};
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
如果是apply,则isFromSyncCommit
为false,所以执行
java
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
这个函数源码:
java
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);
}
}
}
可以看到通过handler来处理。
加载时机
SharedPreferences本质上也是文件,那么何时读取这个文件的呢?
看context的getSharedPreferences
函数(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.
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);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
@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) {
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;
}
可以看到,执行这个函数后,就先在cache中判断是否存在,如果不存在才会取读取文件。所以第一次执行这个函数时就是读取文件的时候(根据name)。后面再执行这个函数(相同name)就会从cache中获取,不必要读取文件了。