Android SharedPreferences 源碼分析
概覽
SharedPreferences(以下使用SP簡稱)在Android中作為一種使用簡單的數據存儲形式被廣泛用來存儲一些不需要做數據庫操作的數據,比如用戶配置項等。本文將從源碼入手分析其實現,并據此提出一些使用中需要注意的事項。
分析
源碼入口
SP是一個interface,首先我們得找到它的具體實現。在SP的使用中,我們通過getSharedPreferences()獲取SP實例如下:
SharedPreferences sp =getSharedPreferences(TAG, MODE);
而getSharedPreferences 是Context接口中的方法。稍微對Android SDK源碼有所了解的朋友都知道,Context的主要實現類——ContextImpl。在該類中,我們可以找到getSharedPreferences方法的具體實現
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
if (sSharedPrefs == null) {
sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
}
final String packageName = getPackageName();
ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();
sSharedPrefs.put(packageName, packagePrefs);
}
// 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";
}
}
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 ||
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;
}
從以上代碼我們可以獲取到以下幾點信息:
- SP的實現類為SharedPreferencesImpl。
- 在ContextImpl中維護了一個從包名到SP字典的字典。
- 傳null作為SP的name是被允許的,它會生成一個以null.xml為名的文件。
- 當sdk version 在3.0以下時支持“MODE_MULTI_PROCESS”模式,在該模式下,每次獲取SP實例時,判斷文件是否被改動,在被改動的情況下重新從文件加載數據,以實現多進程數據同步。但是后來發現即使如此在某些情況下還是不能保證多進程數據一致性,因此就被deprecated了,在3.0以上即使設置了MODE_MULTI_PROCESS也沒有任何作用。
SharedPreferencesImpl
接下來是讓我們移步SharedPreferencesImple,看一下SP的具體實現。
成員變量
//對應的數據文件
private final File mFile;
//在寫操作時備份的文件,如果寫操作失敗,會從備份文件中恢復數據
private final File mBackupFile;
//用戶設置的SP模式
private final int mMode;
//內存中緩存的SP數據
private Map<String, Object> mMap; // guarded by 'this'
//正在做些磁盤操作的進程數
private int mDiskWritesInFlight = 0; // guarded by 'this'
//是否已經加載完成
private boolean mLoaded = false; // guarded by 'this'
文件上一次修改的時間戳和mStatSize 一起被用來判斷文件是否被修改
private long mStatTimestamp; // guarded by 'this'
private long mStatSize; // guarded by 'this'
//寫磁盤鎖
private final Object mWritingToDiskLock = new Object();
//被用來作為mListeners字典中所有鍵的值,意義不明
private static final Object mContent = new Object();
//通過registerOnSharedPreferenceChangeListener注冊進來的listeners,所有的listeners作為鍵緩存在mListeners這個map中,然而所有值都是mContent.
private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners =
new WeakHashMap<OnSharedPreferenceChangeListener, Object>();
初始化過程
以下是SPImpl的構造方法。它初始化了成員變量,并調用startLoadFromDisk()用來初始化數據。
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();
}
在startLoadFromDisk中啟用了一個線程調用了真正的讀文件加載數據方法 —— loadFromDiskLocked。該方法讀取文件并使用XmlUtils類對讀取的數據進行解析。
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;
}
//在寫文件出錯情況下,mFile 文件會被直接delete,這時候就需要從備份文件中恢復數據。
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
// Debugging
if (mFile.exists() && !mFile.canRead()) {
Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
}
Map map = null;
StructStat stat = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16*1024);
map = XmlUtils.readMapXml(str);
} catch (XmlPullParserException e) {
Log.w(TAG, "getSharedPreferences", e);
} catch (FileNotFoundException e) {
Log.w(TAG, "getSharedPreferences", e);
} catch (IOException e) {
Log.w(TAG, "getSharedPreferences", e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
}
mLoaded = true;
//修改成員變量
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<String, Object>();
}
notifyAll();
}
讀取數據
對于SP的使用,我們用的最多的可能就是取數據,以取String類型數據為例,getString方法首先取得SharedPreferencesImpl.this對象鎖,然后同步等待從文件加載數據完成,最后再返回數據。因此這里會有一定的延遲。如果是在UI線程執行SP的取數據操作,可能會對UI流暢度方面造成一定的影響,不過在實踐中我們認為這可以忽略不計。
public String getString(String key, @Nullable String defValue) {
synchronized (this) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
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 {
wait();
} catch (InterruptedException unused) {
}
}
}
存數據
看完讀取數據,接下來再介紹下寫數據。對于SP的寫操作主要是通過Editor接口來完成的,SPImpl中的內部類EditorImpl是Editor的具體實現。
所有的put打頭的方法只是將需要修改的鍵值保存到mModified這個字典中。
public Editor putString(String key, @Nullable String value) {
synchronized (this) {
mModified.put(key, value);
return this;
}
}
所以重頭戲是我們經常使用的commit和apply方法。這兩個方法都是首先修改內存中緩存的mMap的值,然后將數據寫到磁盤中。它們的主要區別是commit會等待寫入磁盤后再返回,而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);
// 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);
}
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。該方法主要根據mModified和是否被clear修改mMap的值,然后返回寫磁盤需要的一些相關值。
private MemoryCommitResult commitToMemory() {
MemoryCommitResult mcr = new MemoryCommitResult();
synchronized (SharedPreferencesImpl.this) {
// 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);
}
mcr.mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
mcr.keysModified = new ArrayList<String>();
mcr.listeners =
new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
synchronized (this) {
if (mClear) {
if (!mMap.isEmpty()) {
mcr.changesMade = true;
mMap.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 (!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;
}
寫磁盤
最后一塊比較重要的就是SP的寫磁盤操作。之前介紹的apply和commit都調用了enqueueDiskWrite()方法。
以下為其具體實現。writeToDiskRunnable中調用writeToFile寫文件。如果參數中的postWriteRunable為null,則該Runnable會被同步執行,而如果不為null,則會將該Runnable放入線程池中異步執行。在這里也驗證了之前提到的commit和apply的區別。
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);
}
Mode相關
下面我們分析一下獲取SP實例時的第二個參數 mode,上文我們已經提到了一個被廢棄的mode——MODE_MULTI_PROCESS。在SPImpl中在writeToFile方法中用到了該參數。
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
再看setFilePermissionsFromMode方法,根據MODE參數設置文件的讀寫權限,而從中我們可以看出能夠造成影響的兩個MODE分別為MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE。而這兩個值在API17之后被Deprecated,所以不建議使用,如果需要多App共享數據,建議使用ContentProvider。
@SuppressWarnings("deprecation")
static void setFilePermissionsFromMode(String name, int mode,
int extraPermissions) {
int perms = FileUtils.S_IRUSR|FileUtils.S_IWUSR
|FileUtils.S_IRGRP|FileUtils.S_IWGRP
|extraPermissions;
if ((mode&MODE_WORLD_READABLE) != 0) {
perms |= FileUtils.S_IROTH;
}
if ((mode&MODE_WORLD_WRITEABLE) != 0) {
perms |= FileUtils.S_IWOTH;
}
if (DEBUG) {
Log.i(TAG, "File " + name + ": mode=0x" + Integer.toHexString(mode)
+ ", perms=0x" + Integer.toHexString(perms));
}
FileUtils.setPermissions(name, perms, -1, -1);
}
總結
以上為對SP代碼的分析,根據以上分析,提出以下幾個在使用SP中需要注意的點。
- MODE_MULTI_PROCESS 無法保證多進程數據一致性,在3.0以上已經沒有任何作用。
- SP從初始化到讀取到數據存在一定延遲,因為需要到文件中讀取數據,因此可能會對UI線程流暢度造成一定影響。
- commit在將數據寫入磁盤后才會返回,而apply則直接返回但無法保證寫磁盤已經完成,只能保證內存中數據的正確性。
- MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE已經被廢棄,不建議使用,如果需要多App共享數據,建議使用ContentProvider,Github上有一個對ContentProvider的封裝Tray,用起來和SP差不多還是蠻不錯的。
來自: http://sixwolf.net/blog/2016/05/03/Android_SharedPreferences源碼分析/