前言
SharePreferences每個開發都一直在用,在用法上沒什麼好說的。但是今天看到一篇總結SharePreferences的文章,覺得總結的挺全的。一些平時大家不怎麼關注的點都講到了。
提出問題
SharedPreferences作爲Android中數據存儲方式的一種,我們經常會用到,它適合用來保存那些少量的數據,特別是鍵值對數據,比如配置信息,登錄信息等。不過要想做到正確使用SharedPreferences,就需要弄清楚下面幾個問題:
(1)每次調用getSharedPreferences時都會新建一個SharedPreferences對象嗎?
(2)在UI線程中調用getXXX有可能導致ANR嗎?
(3)爲什麼SharedPreferences只適合用來存放少量數據,SharedPreferences對應的就是普通的xml文件,爲什麼不能存放大量數據?
(4)commit和apply有什麼區別?
(5)SharedPreferences每次寫入時是增量寫入嗎?
源碼分析
要想弄清楚上面幾個問題,需要查看SharedPreferences的源碼。先從Context的getSharedPreferences開始:
public SharedPreferences getSharedPreferences(String name, int mode) {
return mBase.getSharedPreferences(name, mode);
}
我們知道Android中的Context類體系其實是使用了裝飾者模式,而被裝飾對象就這個mBase,它其實就是一個ContextImpl對象,看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;
}
可以看到這裏使用到了單例模式,sSharedPrefs 是一個ArrayMap,packagePrefs也是一個ArrayMap,它們的關係是這樣的:
packagePrefs存放文件name與SharedPreferencesImpl鍵值對,sSharedPrefs存放包名與ArrayMap鍵值對。注意sSharedPrefs是static變量,也就是一個類只有一個實例,因此你每次getSharedPreferences其實拿到的都是同一個SharedPreferences對象。
這裏回答第一個問題,對於一個相同的SharedPreferences name,獲取到的都是同一個SharedPreferences對象,它其實是SharedPreferencesImpl對象。
SharedPreferencesImpl構造方法:
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();
}
與mBackupFile有關的等後面說,看startLoadFromDisk方法:
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
synchronized (SharedPreferencesImpl.this) {
loadFromDiskLocked();
}
}
}.start();
}
實際上是調用loadFromDiskLocked方法:
private void loadFromDiskLocked() {
if (mLoaded) {
return;
}
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();
}
可以看到對於一個SharedPreferences文件name,第一次調用getSharedPreferences時會去創建一個SharedPreferencesImpl對象,它會開啓一個子線程,然後去把指定的SharedPreferences文件中的鍵值對全部讀取出來,存放在一個Map中。如果我們在UI線程中這樣子寫:
SharedPreferences sp = getSharedPreferences("test", Context.MODE_PRIVATE);
String name = sp.getString("name", null);
調用getString時那個SharedPreferencesImpl構造方法開啓的子線程可能還沒執行完(比如文件比較大時全部讀取會比較久),這時getString當然還不能獲取到相應的值,必須阻塞到那個子線程讀取完爲止,getString方法:
public String getString(String key, String defValue) {
synchronized (this) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
顯然這個awaitLoadedLocked方法就是用來等this這個鎖的,在loadFromDiskLocked方法的最後我們也可以看到它調用了notifyAll方法,這時如果getString之前阻塞了就會被喚醒。那麼現在這裏有一個問題,我們的getString是寫在UI線程中,如果那個getString被阻塞太久了,這時就會出現ANR,因此要根據具體情況考慮是否需要把SharedPreferences的讀寫放在子線程中。
這裏回答第二個問題,在UI線程中調用getXXX可能會導致ANR。同時可以回答第三個問題,SharedPreferences只能用來存放少量數據,如果一個SharedPreferences對應的xml文件很大的話,在初始化時會把這個文件的所有數據都加載到內存中,這樣就會佔用大量的內存,有時我們只是想讀取某個xml文件中一個key的value,結果它把整個文件都加載進來了,顯然如果必要的話這裏需要進行相關優化處理。
SharedPreferences的getXXX的實現基本都是一樣,這裏就不逐個分析了。
SharedPreferences的初始化和讀取比較簡單,寫操作就相對複雜了點,我們知道寫一個SharedPreferences文件都是先要調用edit方法獲取到一個Editor對象:
public Editor edit() {
synchronized (this) {
awaitLoadedLocked();
}
return new EditorImpl();
}
其實拿到的是一個EditorImpl對象,它是SharedPreferencesImpl的內部類:
public final class EditorImpl implements Editor {
private final Map<String, Object> mModified = Maps.newHashMap();
private boolean mClear = false;
......
}
可以看到它有一個Map對象mModified,用來保存“髒數據”,也就是你每次put的時候其實是把那個鍵值對放到這個mModified 中,最後調用apply或者commit纔會真正把數據寫入文件中,比如看putString:
public Editor putString(String key, String value) {
synchronized (this) {
mModified.put(key, value);
return this;
}
}
其它putXXX代碼基本也是一樣的。EditorImpl類的關鍵就是apply和commit,不過它們有一些區別,先看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,再調用enqueueDiskWrite,commitToMemory就是產生一個“合適”的MemoryCommitResult對象mcr,然後調用enqueueDiskWrite時需要把這個對象傳進去,commitToMemory方法:
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;
}
這裏需要弄清楚兩個對象mMap和mModified,mMap是存放當前SharedPreferences文件中的鍵值對,而mModified是存放此時edit時put進去的鍵值對。mDiskWritesInFlight表示正在等待寫的操作數量。可以看到這個方法中首先處理了clear標誌,它調用的是mMap.clear(),然後再遍歷mModified將新的鍵值對put進mMap,也就是說在一次commit事務中,如果同時put一些鍵值對和調用clear,那麼clear掉的只是之前的鍵值對,這次put進去的鍵值對還是會被寫入的。遍歷mModified時,需要處理一個特殊情況,就是如果一個鍵值對的value是this(SharedPreferencesImpl)或者是null那麼表示將此鍵值對刪除,這個在remove方法中可以看到:
public Editor remove(String key) {
synchronized (this) {
mModified.put(key, this);
return this;
}
}
commit接下來就是調用enqueueDiskWrite方法:
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);
}
先定義一個Runnable,注意實現Runnable與繼承Thread的區別,Runnable表示一個任務,不一定要在子線程中執行,一般優先考慮使用Runnable。這個Runnable中先調用writeToFile進行寫操作,寫操作需要先獲得mWritingToDiskLock,也就是寫鎖。然後執行mDiskWritesInFlight–,表示正在等待寫的操作減少1。最後判斷postWriteRunnable是否爲null,調用commit時它爲null,而調用apply時它不爲null。
Runnable定義完,就判斷這次是commit還是apply,如果是commit,即isFromSyncCommit爲true,而且有1個寫操作需要執行,那麼就調用writeToDiskRunnable.run(),注意這個調用是在當前線程中進行的。如果不是commit,那就是apply,這時調用QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable),這個QueuedWork類其實很簡單,裏面有一個SingleThreadExecutor,用於異步執行這個writeToDiskRunnable。
這裏就可以回答第四個問題了,commit的寫操作是在調用線程中執行的,而apply內部是用一個單線程的線程池實現的,因此寫操作是在子線程中執行的。
說一下那個mBackupFile,SharedPreferences在寫入時會先把之前的xml文件改成名成一個備份文件,然後再將要寫入的數據寫到一個新的文件中,如果這個過程執行成功的話,就會把備份文件刪除。由此可見每次即使只是添加一個鍵值對,也會重新寫入整個文件的數據,這也說明SharedPreferences只適合保存少量數據,文件太大會有性能問題。
這裏回答第五個問題,SharedPreferences每次寫入都是整個文件重新寫入,不是增量寫入。
SharedPreferences幾種模式:
Context.MODE_PRIVATE:應用私有,只有相同的UID才能進行讀寫
Context.MODE_MULTI_PROCESS:多進程安全標誌,Android2.3之前該標誌是默認被設置的,Android2.3開始需要自己設置。
MODE_APPEND:首次創建時如果文件存在不會刪除文件。
注意這些模式可以使用位與進行設置,比如MODE_PRIVATE | MODE_APPEND。