從源碼角度硬贯,了解 SharedPreferences 的實現(xiàn)原理,明白為什么 SharedPreferences 會觸發(fā) ANR
簡介
SharedPreferences 是 Android 中比較常用的數(shù)據(jù)存儲方式脑漫,主要用來保存相對較小的『鍵值集合』。
SharedPreferences 的 API 使用簡單方便,如果不了解 SharedPreferences 的實現(xiàn)原理猪钮,
難免會在使用的時候產(chǎn)生各種莫名其妙的問題获印。本文將從源碼的角度述雾,剖析 SharedPreferences 實現(xiàn)原理。
使用方式
-
獲得 SharedPreferences 對象
context.getSharedPreferences(String name, int mode);
也可以在 Activity 中使用
getSharedPreferences(String name, int mode)
-
獲得 Editor 對象
SharedPreferences.Editor editor = sp.edit();
-
寫入數(shù)據(jù)
editor.putInt(key, value);
-
提交數(shù)據(jù)
editor.commit(); editor.apply();
-
讀取數(shù)據(jù)
sp.getString(key,defValue)
源碼分析
先從獲得 SharedPreferences 入手
Activity.getSharedPreferences(String name, int mode) 繼承自 ContextWrapper
public SharedPreferences getSharedPreferences(String name, int mode) {
return mBase.getSharedPreferences(name, mode);
}
其中 mBase 是 Context 對象兼丰。
所以我們最后還是從 context.getSharedPreferences(String name, int mode) 入手
Context 是一個抽象類玻孟,具體的實現(xiàn)類是ContextImpl.java,不了解部分的知識可以參考 Context都沒弄明白鳍征,還怎么做Android開發(fā)黍翎?
public SharedPreferences getSharedPreferences(String name, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
if (sSharedPrefs == null) {
sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
}
……
sp = packagePrefs.get(name);
if (sp == null) {
File prefsFile = getSharedPrefsFile(name);
sp = new SharedPreferencesImpl(prefsFile, mode);
packagePrefs.put(name, sp);
return sp;
}
}
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
……
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
看下 getSharedPrefsFile(name) 方法
public File getSharedPrefsFile(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
private File getPreferencesDir() {
synchronized (mSync) {
if (mPreferencesDir == null) {
mPreferencesDir = new File(getDataDirFile(), "shared_prefs");
}
return mPreferencesDir;
}
}
這里看出
1. getSharedPreferences() 返回的是 SharedPreferencesImpl 對象。
2. 首次調(diào)用 getSharedPreferences() 會初創(chuàng)建 SharedPreferencesImpl 對象艳丛,并緩存匣掸。
3. getSharedPreferences() 方法里面有 synchronized 鎖。
4. SharedPreferences 存儲數(shù)據(jù)的文件為 xml 文件氮双。
通過上面三條可以得出結(jié)論
getSharedPreferences() 首次調(diào)用需要做文件操作碰酝,比較耗時。此時執(zhí)行 SharedPreferences 其他方法可能會出現(xiàn)問題戴差。
SharedPreferencesImpl 對象
先看一下構(gòu)造函數(shù)
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();
}
makeBackupFile 只是創(chuàng)建了一個文件
private static File makeBackupFile(File prefsFile) {
return new File(prefsFile.getPath() + ".bak");
}
再看 startLoadFromDisk()
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
synchronized (SharedPreferencesImpl.this) {
loadFromDiskLocked();
}
}
}.start();
}
private void loadFromDiskLocked() {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
……
Map map = null;
StructStat stat = null;
……
map = XmlUtils.readMapXml(str);
……
mLoaded = true;
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<String, Object>();
}
notifyAll();
}
這里就可以看到送爸,通過 XmlUtils.readMapXml(str) 把 xml 文件轉(zhuǎn)換為一個 HashMap。
總結(jié)一下流程圖如下
獲得 Editor 對象
public Editor edit() {
synchronized (this) {
awaitLoadedLocked();
}
return new EditorImpl();
}
private void awaitLoadedLocked() {
if (!mLoaded) {
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
wait();
} catch (InterruptedException unused) {
}
}
}
這里可以看出 edit() 返回的其實是一個 EditorImpl 對象造挽,如果執(zhí)行 edit() 時如果 SharedPreferencesImpl 沒有加載完成碱璃,就會阻塞。
EditorImpl 構(gòu)造函數(shù)沒有什么特殊之處饭入。
寫入數(shù)據(jù)
public Editor putString(String key, @Nullable String value) {
synchronized (this) {
mModified.put(key, value);
return this;
}
}
寫入數(shù)據(jù)比較簡單嵌器,就是把數(shù)據(jù)存儲到 mModified 中
private final Map<String, Object> mModified = Maps.newHashMap();
提交數(shù)據(jù)
寫入數(shù)據(jù)以后并不會立刻被存儲到文件之中,需要經(jīng)過『提交』操作才能寫入文件谐丢。
SharedPreferences 有兩種提交方式 editor.commit()
爽航、editor.apply()
editor.commit()
public boolean commit() {
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
先看 commitToMemory() 方法
// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
MemoryCommitResult mcr = new MemoryCommitResult();
synchronized (SharedPreferencesImpl.this) {
……
if (mDiskWritesInFlight > 0) {
……
mMap = new HashMap<String, Object>(mMap);
}
……
synchronized (this) {
……
for (Map.Entry<String, Object> e : mModified.entrySet()) {
……
if (v == this || v == null) {
if (!mMap.containsKey(k)) {
continue;
}
mMap.remove(k);
} else {
if (mMap.containsKey(k)) {
Object existingValue = mMap.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mMap.put(k, v);
}
mcr.changesMade = true;
if (hasListeners) {
mcr.keysModified.add(k);
}
}
mModified.clear();
}
}
return mcr;
}
這里可以看出 commitToMemory 是把 EditorImpl 中的 mModified 做一個深層拷貝給 SharedPreferencesImpl 的 mMap 對象蚓让。
再看
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr);
}
synchronized (SharedPreferencesImpl.this) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
final boolean isFromSyncCommit = (postWriteRunnable == null);
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (SharedPreferencesImpl.this) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
可以看到因為 postWriteRunnable ==null ,所以 任務(wù)會在當(dāng)前線程直接提交。
private void writeToFile(MemoryCommitResult mcr) {
// Rename the current file so it may be used as a backup during the next read
if (mFile.exists()) {
……
}
……
try {
FileOutputStream str = createFileOutputStream(mFile);
……
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
FileUtils.sync(str);
……
}
writeToFile 及時把 map 對象存儲的信息寫入到文件中讥珍。
editor.apply()
public void apply() {
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
QueuedWork.add(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.remove(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
……
notifyListeners(mcr);
}
apply() 和 commit() 的區(qū)別就是執(zhí)行 enqueueDiskWrite(mcr, postWriteRunnable) 方法的時候 postWriteRunnable 不為空历极,
所以 apply() 方法提交數(shù)據(jù)的時候是在 QueuedWork 維護的單線程池中。
讀取數(shù)據(jù)
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (this) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
讀取數(shù)據(jù)比較簡單衷佃,就是從 SharedPreferencesImpl 的 mMap 中獲得數(shù)據(jù)趟卸。
最后用一個集合以上操作的時序圖總結(jié)。