前几天遇到了一个使用SharedPreferences(以下简写成sp)导致严格模式报警的问题,我打点计算了一下sp#getInt()的时间,发现sp还是挺慢的,大约30ms。今天有点空闲,我们来研究一下sp的原理。
(源码基于android-28)
我接下来会顺着我们使用sp的步骤,一步一步的来分析。
主要包含:sp的创建、sp的数据读取、sp的写入。
sp的创建
通话context#getSharedPreferences可以获取到sp实例。
android.content.Context#getSharedPreferences(java.io.File, int)
public abstract SharedPreferences getSharedPreferences(File file, @PreferencesMode int mode);
通过源码可以看出,这里的具体实现应该是在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<>(); // sp path的内存缓存
}
file = mSharedPrefsPaths.get(name);
if (file == null) {
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; //SharedPreferencesImpl 真正的实现类
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked(); // 缓存
sp = cache.get(file); // 从缓存中拿到sp impl
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); // 创建 sp impl
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;
}
@GuardedBy("ContextImpl.class")
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;
}
所以sharedPreferences的真正实现在SharedPreferencesImpl。
再来看一下SharedPreferencesImpl的初始化过程
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里边
synchronized (mLock) {
mLoaded = true; // 标记数据加载完成
mThrowable = thrown;
....
try {
.... // 初始化mMap,正常情况下会 mMap = map
} catch (Throwable t) {
mThrowable = t;
} finally {
mLock.notifyAll(); // 释放锁
}
}
}
从上面的代码我们可以发现,sp的创建过程虽然复杂,但是耗时操作已经放到了子线程,所以我们在获取sp实例的时候不需要担心阻塞的问题。
同时,我们注意到有两个变量比较重要:mLoaded和mLock,这两个变量是为了保证必须在数据从disk上加载完毕后,我们才能对sp进行读写操作。
sp的数据读取
sharedPreferences.getInt() // 这是一种常用的获取sp数据的方式
来看一下具体的实现:
android.app.SharedPreferencesImpl#getInt
@Override
public int getInt(String key, int defValue) {
synchronized (mLock) { // 这里用了mLock,保证上边的初始化和现在的读取互斥
awaitLoadedLocked(); // 检查或等待数据加载完成
Integer v = (Integer)mMap.get(key);
return v != null ? v : defValue;
}
}
@GuardedBy("mLock")
private void awaitLoadedLocked() {
if (!mLoaded) {
// Raise an explicit StrictMode onReadFromDisk for this
// thread, since the real read will be in a different
// thread and otherwise ignored by StrictMode.
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) { // 如果没有加载完成,
try {
mLock.wait(); // 等待加载完成,这里会阻塞线程
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
所以,因为awaitLoadedLocked的存在,我们在getInt的时候,可能会有一定的耗时。只要你不是刚初始化sp,就立刻去get数据,一般也不会阻塞。
sp的写入
sharedPreferences.edit().putInt().apply();
sharedPreferences.edit().putInt().commit();
sp的写入有两种方式,apply和commit。
这里有个新的类:Editor,他的实现在android.app.SharedPreferencesImpl.EditorImpl,我们重点关注apply和commit方法。
@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); //这是个内部工具类,用于跟踪那些未完成的或尚未结束的全局任务,新任务通过方法 queue 加入。添加 finisher 的runnables,由 waitToFinish 方法保证执行,用于保证任务已被处理完成。
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); // 真正的异步写入disk
// 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
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) { // 使用sp中的那个数据访问锁,保证线程安全
// 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);
}
mapToWriteToDisk = mMap; // 拿到sp中的map
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;
}
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); //写入数据到map中,即内存中
} else {
if (mapToWriteToDisk.containsKey(k)) {
Object existingValue = mapToWriteToDisk.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mapToWriteToDisk.put(k, v); //写入数据到map中,即内存中
}
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
@Override
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
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");
}
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
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);
}
@GuardedBy("mWritingToDiskLock")
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
.... //写入相关的
if (!backupFileExists) {
if (!mFile.renameTo(mBackupFile)) {
Log.e(TAG, "Couldn't rename file " + mFile
+ " to backup file " + mBackupFile);
mcr.setDiskWriteResult(false, false); // 对CountDownLatch进行countDown,释放阻塞
return;
}
} else {
mFile.delete();
}
}
}
所以apply是先提交到内存,异步存储到disk。而commit是先提交到内存后,然后阻塞线程到存储disk完成。
QueuedWork
这里需要补充一下QueuedWork,这个类我们用不到,因为是@hide,我们需要知道的是这个queue是异步的,是通过Handler实现的,线程名称是queued-work-looper。感觉这个代码比较关键:
/**
* Lazily create a handler on a separate thread.
*
* @return the handler
*/
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;
}
}
以上是所有内容,The End.
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!