前言
???????Sharedpreferences是Android平臺上一個輕量級的存儲類,用來保存應用程序的各種配置信息,其本質是一個以“key-value”鍵值對的方式保存數據的xml文件,其文件保存在/data/data/com.xx.xxx/shared_prefs目錄下,Android其它四種存儲方式為:
???????a. ContentProvider:將應用的私有數據提供給其他應用使用;
???????b. 文件存儲:以IO流形式存放,可分為手機內部和手機外部(sd卡等)存儲,可存放較大數據;
???????c. SQLiteDataBase:輕量級、跨平臺數據庫,將所有數據都是存放在手機上的單一文件內,占用內存小;
???????d. 網絡存儲:數據存儲在服務器上,通過連接網絡獲取數據;
???????由于使用方式比較簡單,平常的Android應用開發中經常用到Sharedpreferences,本文結合源碼對平常使用及可能會出現的問題進行一一分析。
一.獲取Sharedpreferences
???????要想使用 SharedPreferences 來存儲數據,首先需要獲取到 SharedPreferences 對象,平常最常用的是通過以下方式來獲取Sharedpreferences對象,通過調用context的getSharedPreferences()方法:
SharedPreferences mPreference = mContext.getSharedPreferences(SharePreferenceUtils.PREFERENCE_NAME,
Context.MODE_PRIVATE);
???????還有另外兩種方式,一種是在Activity內部直接通過getPreferences()來獲取該Activity類名對應name的SharedPreferences xml文件;一種是通過 PreferenceManager中的getDefaultSharedPreferences()靜態方法,它接收一個 Context 參數,并自動使用當前應用程序的包名作為前綴來命名SharedPreferences xml文件;
二.使用方式
???????SharedPreferences對象本身只能獲取數據而不支持存儲和修改,存儲修改是通過SharedPreferences.edit()獲取的內部接口Editor對象實現。使用Editor來存取數據,用到了SharedPreferences接口和SharedPreferences的一個內部接口SharedPreferences.Editor,使用方式如下:
a.寫入數據:
//1.創建一個SharedPreferences對象
SharedPreferences sharedPreferences= getSharedPreferences(SharePreferenceUtils.PREFERENCE_NAME,
Context.MODE_PRIVATE);
//2.實例化SharedPreferences.Editor對象
SharedPreferences.Editor editor = sharedPreferences.edit();
//3.將獲取過來的值放入文件
editor.putString("xxx", “xxx”);
editor.putInt("xxx", 10);
editor.putBoolean("xxx",false);
//4.提交
editor.commit();
editor.apply();
b.讀取數據:
SharedPreferences sharedPreferences = getSharedPreferences(SharePreferenceUtils.PREFERENCE_NAME,
Context.MODE_PRIVATE);
String name = sharedPreferences.getString("xxx","");
c.刪除指定數據
editor.remove("xxx");
editor.commit();
d.清空數據
editor.clear();
editor.commit();
三.源碼分析
???????接下來根據調用關系,深入源碼一起來詳細的了解一下,看一下平時使用中可能遇到的問題及問題原因,本文主要對通過context.getSharedPreferences()方式來獲取時的調用順序及涉及到的類進行一一分析:
a.ContextImpl
private ArrayMap<String, File> mSharedPrefsPaths;
public SharedPreferences getSharedPreferences(String name, int mode) {
......
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);
}
public File getSharedPreferencesPath(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
???????以上可以看到,在getSharedPreferences()內部會有一個Map緩存,如果Map緩存中有對應name的File,直接獲取;否則的話,會通過getSharedPreferencesPath()來創建File,然后存入Map;最后通過getSharedPreferences()來返回SharedPreferences。
@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);
......
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) {
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
???????在getSharedPreferences內部返回的是一個SharedPreferencesImpl對象,SharedPreferences是一個接口,SharedPreferencesImpl是接口的實現類。
b.SharedPreferencesImpl
???????通過獲取方式可以看到,最終返回的是SharedPreferencesImpl對象,那么所有的操作都是通過該類來實現的,一起看一下:
private final File mFile;
private final File mBackupFile;
private final int mMode;
private Map<String, Object> mMap;
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();
}
static File makeBackupFile(File prefsFile) {
return new File(prefsFile.getPath() + ".bak");
}
???????在獲取SharedPreferencesImpl對象時,主要做了對應的賦值操作,執行了makeBackupFile()來備份文件,重點來看一下startLoadFromDisk():
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
???????在startLoadFromDisk()內部現將mLoaded置為false,然后啟動異步線程來執行loadFromDisk(),再看一下loadFromDisk():
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) {
return;
}
//檢查源文件的備份文件是否存在:mBackupFile.exists(),如果存在,則將源文件刪除:mFile.delete(),
//然后將備份文件修改為源文件:mBackupFile.renameTo(mFile)。
//后續操作就是從備份文件加載相關數據到內存 mMap 容器中了
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
......
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);
} .......
}
}
synchronized (mLock) {
mLoaded = true;
if (map != null) {
mMap = map;
......
} else {
mMap = new HashMap<>();
}
mLock.notifyAll();
}
}
???????在loadFromDisk()內部主要干了五件事:
???????1.如果備份文件存在,那么就先刪除mFile文件,然后將備份文件命名為mFile;
???????2.通過XmlUtils.readMapXml將xml文件內的鍵值對讀取為Map形式存在;
???????3.加鎖,將mLoaded賦值為true,表示已經加載完畢;
???????4.如果map不為null,將讀取的內容賦值給mMap(構造方法內置為null);
???????5.調用mLock.notifyAll()來通知;
???????以上就是通過getSharedPreferences()來獲取SharedPreferences對象的過程,上述的3、4、5是簡單的賦值或通知操作,為什么要標注出來呢,接著往下看,看一下讀取操作:
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
???????以上可以看到,在獲取sp內的值時,先進行加鎖操作,然后執行了awaitLoadedLocked(),從字面意思來看,是等待加載完成,那看一下內部實現:
private void awaitLoadedLocked() {
......
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
}
???????我們可以看到,如果mLoaded為false,那么會執行mLock.wait(),即進行等待,接下來就用到了上述講的在loadFromDisk()內部最后賦值和通知操作了,如果在get()時mLoaded為false,那么表示沒有加載完畢,就需要一直等待,等在loadFromDisk()內部加載完畢后才會結束等待,那么問題來了:為什么SharedPreference會造成卡頓甚至ANR?
???????第一次從SharedPreference獲取值的時候,可能阻塞主線程,造成卡頓/丟幀。獲取SharedPreference可以立刻返回,耗時操作是在異步線程,但是去獲取時如果異步線程沒有加載完畢,會一直等待,造成卡頓,如果等待時間超過5s,甚至會造成ANR。注意:只有第一次才會,后面不會,因為加載文件成功后會在內存緩存數據,下次就不需要等待了。
???????上面講到了獲取SharedPreference對象及獲取sp存儲的值,那么如何寫入值呢?前面講到通過edit()獲取到Editor來進行寫操作:
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
???????每次執行edit()也需要等待鎖,然后new一個EditorImpl對象,Editor是一個接口,內部的邏輯最終是通過其實現類EditorImpl來完成的,一起看一下:
c.EditorImpl
public final class EditorImpl implements Editor {
private final Object mLock = new Object();
@GuardedBy("mLock")
private final Map<String, Object> mModified = Maps.newHashMap();
public Editor putString(String key, @Nullable String value) {
synchronized (mLock) {
mModified.put(key, value);
return this;
}
}
......
public Editor remove(String key) {
synchronized (mLock) {
mModified.put(key, this);
return this;
}
}
public Editor clear() {
synchronized (mLock) {
mClear = true;
return this;
}
}
}
???????在向sp存數據時,先執行put相關的操作,接下來需要執行apply()及commit()來進行寫入操作,那apply()和commit()有什么區別呢?
???????1.apply()是異步操作,commit()是同步操作;
???????2.apply()無返回值,commit()有boolean返回值;
???????先看一下apply():
public void apply() {
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
//阻塞等待寫入文件完成,否則阻塞在這
//利用CountDownLatch來等待任務的完成
//后面執行enqueueDiskWrite寫入文件成功后會把writtenToDiskLatch多線程計數器減1,
//這樣的話下面的阻塞代碼就可以通過了.
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
.......
}
};
//QueuedWork是用來確保SharedPrefenced的寫操作在Activity銷毀前執行完的一個全局隊列.
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
//執行阻塞任務
awaitCommit.run();
//阻塞完成之后,從隊列中移除任務
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
???????以上可以看到,在apply()中執行的操作為:
???????1.通過commitToMemory()創建MemoryCommitResult;
???????2.創建一個Runnable awaitCommit;
???????3.將awaitCommit執行addFinisher()[此處會有問題];
???????4.創建一個Runnable postWriteRunnable;
???????5.執行enqueueDiskWrite(),真正的寫入操作;
???????6.執行notifyListeners()通知sp數據變化;
???????接下來看一下commit():
public boolean commit() {
.......
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite( mcr, null /* sync write on this thread okay */);
try {
mcr.writtenToDiskLatch.await();
}.......
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
???????以上可以看到,在commit()中執行的操作為:
???????1.通過commitToMemory()創建MemoryCommitResult;
???????2.執行enqueueDiskWrite();
???????3.執行mcr.writtenToDiskLatch.await()[apply是在子線程中完成的];
???????4.執行notifyListeners()通知sp數據變化;
???????5.最后返回writeToDiskResult;
???????上述可以看到,在apply()及commit()中都通過commitToMemory()創建了MemoryCommitResult,看一下commitToMemory()內部執行邏輯:
private MemoryCommitResult commitToMemory() {
......
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
if (mDiskWritesInFlight > 0) {
mMap = new HashMap<String, Object>(mMap);
}
mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
.........
synchronized (mLock) {
boolean changesMade = false;
if (mClear) {
if (!mMap.isEmpty()) {
changesMade = true;
mMap.clear();
}
mClear = false;
}
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
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);
}
//數據有變化,將changesMade置為true;
changesMade = true;
....
}
mModified.clear();
if (changesMade) {
//將該值+1,后續writeToFile()更新會用到
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
???????以上可以看到,在commitToMemory()中執行的操作為:
???????1.將mMap賦值給局部變量mapToWriteToDisk[執行文件寫入時會用到],mDiskWritesInFlight++[commit操作會調用到];
???????2.如果clear為true,即當執行了clear()操作,會將mMap清空;
???????3.遍歷mModified,當v==this,說明執行了remove()操作,則將該值在mMap中移除,其他情況,即執行put()操作,先判斷是否包含key,然后對比value,最后確定是否需要將值put到mMap中;
???????4.將mModified清空,準備下次put操作;
???????5.最后new MemoryCommitResult()返回;
???????通過以上我們可以看到,當執行put()、remove()、clear()操作時,mMap是實時更新的,那么什么時候寫入xml中的呢?在apply()及commit()中都執行了SharedPreferencesImpl.this.enqueueDiskWrite(mcr,postWriteRunnable),commit()時傳入的postWriteRunnable為NULL,一起看一下enqueueDiskWrite()這個方法:
private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
???????以上可以看到,在enqueueDiskWrite()中執行的操作為:
???????1.先判斷postWriteRunnable是否為null,如果為null,說明是同步操作,即執行的commit()操作;
???????2.創建了writeToDiskRunnable,在內部執行了writeToFile(),然后執行了postWriteRunnable;
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
.......
if (fileExists) {
boolean needsWrite = false;
//判斷是否需要寫入文件,如果值沒有變化的話,就不需要寫入,在commitToMemory()中有對
//memoryStateGeneration相關的操作
if (mDiskStateGeneration < mcr.memoryStateGeneration) {
if (isFromSyncCommit) {
needsWrite = true;
} else {
synchronized (mLock) {
if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
needsWrite = true;
}
}
}
}
if (!needsWrite) {
//不需要wirte,直接setDiskWriteResult(),減少IO操作,commit()時會用來返回值,正常下返回true;
mcr.setDiskWriteResult(false, true);
return;
}
//判斷備份文件是否存在
boolean backupFileExists = mBackupFile.exists();
//備份文件不存在,則在寫入前需要先備份文件
if (!backupFileExists) {
if (!mFile.renameTo(mBackupFile)) {
//備份失敗,直接返回
mcr.setDiskWriteResult(false, false);
return;
}
} else {
//如果備份文件存在,將源文件刪除[正常下備份文件是不應該存在的]
mFile.delete();
}
}
try {
//創建mFile文件的輸入流
FileOutputStream str = createFileOutputStream(mFile);
if (str == null) {
mcr.setDiskWriteResult(false, false);
return;
}
//向xml中寫入值
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
//強制更新到xml文件
FileUtils.sync(str);
str.close();
........
//寫入成功,將備份文件刪除
mBackupFile.delete();
//賦值,為了下次的判斷
mDiskStateGeneration = mcr.memoryStateGeneration;
mcr.setDiskWriteResult(true, true);
mNumSync++;
return;
} ......
//寫入失敗,將源文件刪除
if (mFile.exists()) {
if (!mFile.delete()) {
Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
}
}
mcr.setDiskWriteResult(false, false);
}
???????3.如果是同步操作,直接在主線程執行postWriteRunnable,返回;
???????4.如果是異步操作,即apply(),會將postWriteRunnable加入到QueuedWork隊列中;
d.QueuedWork
private static final LinkedList<Runnable> sWork = new LinkedList<>();
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);
}
}
}
???????以上可以看到,在執行QueuedWork的queue()操作:
???????1.通過getHandler()獲取Handler;
???????2.將Runnable work加入到sWork鏈表中;
???????3.delay 100ms執行MSG_RUN消息;
???????此時還沒有看到什么時候執行的Runnable,接著往下看:
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;
}
}
private static class QueuedWorkHandler extends Handler {
static final int MSG_RUN = 1;
QueuedWorkHandler(Looper looper) {
super(looper);
}
public void handleMessage(Message msg) {
if (msg.what == MSG_RUN) {
processPendingWork();
}
}
}
private static void processPendingWork() {
......
synchronized (sProcessingWork) {
LinkedList<Runnable> work;
synchronized (sLock) {
work = (LinkedList<Runnable>) sWork.clone();
sWork.clear();
// Remove all msg-s as all work will be processed now
getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
}
if (work.size() > 0) {
for (Runnable w : work) {
w.run();
}
}
}
}
???????以上可以看到,在getHandler()內部會創建HandlerThread,通過消息隊列,最終會執行Runnable的run()方法,然后里面執行耗時的WriteToFile()操作。
???????至此,SharedPreferences相關的原理就分析完了。
四.總結
a.加載緩慢
???????SharedPreferences文件的加載使用了異步線程,而且加載線程并沒有設置優先級,如果在加載時讀取數據就需要等待文件加載線程的結束。這就導致主線程等待低優先線程鎖的問題,建議提前用預加載啟動過程用到的SP文件。
b.commit和apply有什么區別?
???????commit()是同步且有返回值;apply()方法是異步沒有返回值;
???????commit()在主線程寫入文件,會造成UI卡頓;apply()在子線程寫入文件,也有可能卡UI;
???????apply()是在子線程寫文件并不會造成UI線程卡頓,但是在ActivityThread的handlePauseActivity()、handleStopActivity()等方法中都會調用到:
QueuedWork.waitToFinish();
???????看一下QueuedWork的waitToFinish()方法:
public static void waitToFinish() {
.......
try {
while (true) {
Runnable finisher;
synchronized (sLock) {
finisher = sFinishers.poll();
}
if (finisher == null) {
break;
}
finisher.run();
}
} finally {
sCanDelay = true;
}
}
......
???????以上可以看到,循環地從sFinishers這個隊列中取任務執行,直到任務為空。這個任務就是之前apply()中的awaitCommit,它是用來等待寫入文件的線程執行完畢的。如果因為多次執行apply(),在onPause()時,那就意味著寫入任務會在這里排隊,但是寫入文件那里只有一個HandlerThread在串行的執行,那是不是就卡頓了?
c.合并操作
???????每次執行put()都會通過edit()創建一個EditorImpl對象,然后都會執行writeToFile()寫文件操作,所以如果執行多次操作的話,建議合并為一次。
SharedPreferences.Editor editor = mPreference.edit();
editor.putInt(key,status).putString(key1,status1).putboolean(key2,status1).apply();
d.SharedPreference如何跨進程通信?
* @see #getSharedPreferences
*
* @deprecated MODE_MULTI_PROCESS does not work reliably in
* some versions of Android, and furthermore does not provide any
* mechanism for reconciling concurrent modifications across
* processes. Applications should not attempt to use it. Instead,
* they should use an explicit cross-process data management
* approach such as {@link android.content.ContentProvider ContentProvider}.
*/
@Deprecated
public static final int MODE_MULTI_PROCESS = 0x0004;
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();
}
???????在初始化sp的時候,設置flag為MODE_MULTI_PROCESS來跨進程通信,但是很遺憾,這種方式已經被廢棄。
e.全量寫入
???????無論是 commit() 還是 apply(),即使我們只改動其中一個條目,都會把整個內容全部寫到文件。而且即使我們多次寫同一個文件,SP也沒有將多次修改合并為一次,這也是性能差的重要原因之一。
???????總而言之,SharedPreferences是用來存儲一些非常簡單、輕量的數據。我們不要使用它存儲過于復雜的數據,例如 HTML、JSON 等。而且 SharedPreferences 的文件存儲性能與文件大小有關,每個 SP 文件不能過大,不要將毫無關聯的配置項保存在同一個文件中,同時考慮將頻繁修改的條目單獨隔離出來。