SharedPreferences(簡稱sp)Android平臺上一個輕量級的存儲輔助類,它提供了key-value鍵值對的接口,用來保存應用的一些常用配置,在應用中通常做一些簡單數據的持久化緩存。本文將詳細的分析SharedPreferences的實現方式、存儲機制、如何正確使用它以及sp的性能問題等方面。
SharedPreferences實現詳解
我們在Android開發中,如果想要保存一個相對較小的鍵值對集合,則應使用SharedPreferences API。SharedPreferences對象指向包含鍵值對的文件,並提供讀寫這些鍵值對的簡單方法。SharedPreferences API提供了string,set,int,long,float,boolean六種數據類型的數據訪問接口。sp文件在存儲區最終數據是以xml形式進行存儲。
獲取SharedPreferences對象
想要使用sp來存取數據,我們首先要了解如何去獲取它,Android的Context類爲我們提供了獲取SharedPreferences對象的抽象接口。
Context對象的getSharedPreferences()方法可以獲取一個SharedPreferences對象,之後我們就可以通過SharedPreferences來管理我們的鍵值對數據了。
Context中的方法定義:
public abstract SharedPreferences getSharedPreferences(File file, @PreferencesMode int mode);
context類爲我們提供了訪問sp的抽象接口,真正的而實現實在ContextImpl類中。
ContextImpl中的方法實現:
我們來看源碼實現:
@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);
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);
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;
}
參數:
- file參數用於指定SharedPreferences文件的名稱,如果指定的文件不存在則會創建一個,SharedPreferences文件都是存放在“/data/data/<包名>/shared_prefs/”目錄下。
- mode參數用於指定操作模式,它的可選值有2種,MODE_PRIVATE(默認值,指定文件是私有的,只可被當前應用訪問或者相同user ID的進程)和MODE_MULTI_PROCESS(多進程共享模式)。
邏輯解析:
- getSharedPreferences首先會從內存緩存中查找是否已經存在我們想要獲取的SharedPreferences實例。
- 如果不存在,則會爲相應的file創建一個SharedPreferencesImpl實例,並且將它添加到緩存中。
- 判斷如果參數mode的值是Context.MODE_MULTI_PROCESS或者App的targetSdkVersion小於Android 3.0,則執行調用sp的startReloadIfChangedUnexpectedly方法執行sp文件的重新加載工作,我們稍後分析。
MODE_MULTI_PROCESS模式
我們通常調用getSharedPreferences方法時,使用默認模式即可,也是google推薦的方式。Context.MODE_MULTI_PROCESS是多線程共享模式,理論上可以做到多進程數據共享功能,但是,此功能已廢棄,不建議使用了。Context.MODE_MULTI_PROCESS模式是在Android 3.0之前版本的遺留功能,在之後某些版本的Android中無法可靠工作,而且也不提供任何協調跨進程併發修改的機制。我們不應該嘗試使用該模式,如果我們有跨進程數據傳輸的需求,應該使用明確的跨進行數據共享機制,例如ContentProvider等來實現。
另外,MODE_MULTI_PROCESS模式需要進程在每次訪問數據時都要進行io操作,以檢查數據是否被其他進程改變,這樣io操作造成了很大的性能消耗,如果我們在開發中誤使用了該模式,應立即改成默認模式!
sp數據文件從存儲分區加載到內存過程分析
SharedPreferences工具類爲我們提供了管理sp數據的接口,從而簡化了數據存取操作。sp數據文件最終是以.xml文件的格式,存儲到App數據私有目錄:/data/data/<app包名>/shared_prefs/目錄下,那麼sp文件是如何從存儲區加載到內存中的呢?
在Context的getSharedPreferences方法獲取SharedPreferences對象時,我們發現如果參數mode的值是Context.MODE_MULTI_PROCESS或者App的targetSdkVersion小於Android 3.0,則執行調用sp的startReloadIfChangedUnexpectedly方法執行sp文件的重新加載工作,這裏就涉及到了sp文件加載過程,同樣SharedPreferencesImpl類的構造過程也會涉及到sp文件的加載。
android.app.SharedPreferencesImpl:
@UnsupportedAppUsage
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);//這裏注意,創建了一個備份文件。
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk(); //加載xml文件到內存中
}
SharedPreferencesImpl類的startReloadIfChangedUnexpectedly方法:
@UnsupportedAppUsage
void startReloadIfChangedUnexpectedly() {
synchronized (mLock) {
// TODO: wait for any pending writes to disk?
if (!hasFileChangedUnexpectedly()) {
return;
}
startLoadFromDisk();
}
}
SharedPreferencesImpl的構造方法和startReloadIfChangedUnexpectedly都調用到了startLoadFromDisk()方法。
startLoadFromDisk()方法:
@UnsupportedAppUsage
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
邏輯解析:
- startLoadFromDisk方法中,先重置mLoaded狀態。
- 創建一個工作線程,線程名爲SharedPreferencesImpl-load。
- 線程調用loadFromDisk()方法執行sp文件從存儲分區到內存的加載工作。
loadFromDisk()方法:
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) { //判斷mLoaded狀態
return;
}
if (mBackupFile.exists()) { //如果備份文件存在,則代表原始文件數據出現錯誤,使用備份文件,替換掉原始文件。
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
……
Map<String, Object> map = null;
StructStat stat = null;
Throwable thrown = null;
try {
stat = Os.stat(mFile.getPath()); //文件存在
if (mFile.canRead()) { //文件可讀取
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16 * 1024); //獲取輸入流
map = (Map<String, Object>) XmlUtils.readMapXml(str);//解析xml文件到map中
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
// An errno exception means the stat failed. Treat as empty/non-existing by
// ignoring.
} catch (Throwable t) {
thrown = t;
}
synchronized (mLock) {
mLoaded = true;
mThrowable = thrown;
// It's important that we always signal waiters, even if we'll make
// them fail with an exception. The try-finally is pretty wide, but
// better safe than sorry.
try {
if (thrown == null) {
if (map != null) { //一切順利,這裏開始賦值
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
}
// In case of a thrown exception, we retain the old map. That allows
// any open editors to commit and store updates.
} catch (Throwable t) {
mThrowable = t;
} finally {
mLock.notifyAll();
}
}
}
邏輯解析:
- 判斷備份文件是否存在,如果存在則代表原始文件數據出現錯誤,使用備份文件,替換掉原始文件。
- 如果文件存在並且可讀取,則把字節流讀取到內存中,並且使用XmlUtils.readMapXml工具方法對原始數據進行解析。
- 數據解析後得到一個Map對象,它保存了該sp文件中存儲的所有鍵值對的信息。
- 最後把得到的Map對象賦值給mMap屬性,mStatTimestamp和mStatTimestamp用於判斷數據是否更新。
SharedPreferences數據在內存中的存儲結構
SharedPreferences文件都是存放在“/data/data/<app包名>/shared_prefs/”目錄下。sp文件的存儲格式是.xml文件,當SharedPreferences文件創建時,就會在相應目錄新建一個本地文件。
我們可以從ContextImpl中看到sp文件是如何管理的:
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
這裏會定義一個ArrayMap成員變量,存儲了當前應用的所有sp對象。這樣做是系統爲了性能考慮,在每個sp文件讀取之後,都會把sp對象存儲到一個map中作爲緩存。
ArrayMap對象的創建及賦值過程:
@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對象數據的存儲結構是什麼。
SharedPreferences對象數據的存儲結構:
- 首先以包名爲key,存儲一個名爲sSharedPrefsCache的ArrayMap隊列中。
- 在ArrayMap中,又是以文件名爲key,SharedPreferencesImpl對象(sp的實例對象)爲value存儲的。
- 每一個SharedPreferencesImpl對象都對應一個物理位置在的sp文件。
- SharedPreferencesImpl對象在創建時,或者sp文件有更新時,都會去存儲區同步數據文件,並且把數據文件存儲到一個map中(key/vaule分別對應我們存儲時的key/value),該map就是上文中提到的SharedPreferencesImpl對象的mMap屬性。
SharedPreferences的數據讀取過程分析
我們以獲取int值爲例,來看sp的數據讀取過程。
SharedPreferencesImpl類的getInt方法:
@Override
public int getInt(String key, int defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Integer v = (Integer)mMap.get(key);
return v != null ? v : defValue;
}
}
SharedPreferencesImpl類的getInt方法可以獲取一個在sp文件中存儲的int值。這裏可以看到,源碼中是直接從mMap中讀取的,而這個mMap是SharedPreferencesImpl在創建時初始化的。這種做法,可以避免每次讀取時,系統和存儲分區的交互,從而大幅度提升了性能。
其他幾種類型的數據讀取邏輯類似,這裏可以看到,讀取過程相對來說非常簡單,當SharedPreferencesImpl實例創建完成後,sp的xml文件中的數據已經加載到內存中,所以這裏獲取時,只需要簡單的內存查詢即可。
SharedPreferences數據存儲過程分析
數據存儲過程相對來說比較複雜,我們先來看如何使用sp來實現存儲。
SharedPreferences數據存儲示例
如果我們想要通過SharedPreferences存儲數據,代碼如下:
SharedPreferences.Editor editor = getSharedPreferences("person", MODE_PRIVATE).edit();
editor.putString("name","budaye");
editor.putInt("age",18);
editor.apply();
代碼執行之後,系統會在/data/data/<app包名>/shared_prefs/目錄下,創建一個名爲person.xml的文件:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="name">budaye</string>
<int name="age" value="18" />
</map>
sp的數據存儲,需要藉助SharedPreferences的內部類Editor來實現,並且最後要使用apply()或commit()來保存更改。
我們來看源碼。
SharedPreferences.Editor的putInt方法分析
我們以putInt爲例,來分析sp數據的存儲過程。
SharedPreferences的edit()方法:
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();//等待鎖釋放
}
return new EditorImpl();
}
該方法new一個EditorImpl對象並返回,所以SharedPreferences.Editor的實現類是EditorImpl。
EditorImpl的putInt方法
@GuardedBy("mEditorLock")
private final Map<String, Object> mModified = new HashMap<>();
@Override
public Editor putInt(String key, int value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
這裏只是簡單的將key和value的值put到了mModified中,mModified是一個Map,它存儲者一次事務提交的所有將要變更的數據列表。
EditorImpl的apply方法
EditorImpl的apply方法:
@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);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
……
notifyListeners(mcr);
}
邏輯解析:
- 調用commitToMemory()來把數據更新至內存中。
- apply()方法使用異步的方式來實現數據的寫入過程(寫入存儲分區)。
- 調用SharedPreferencesImpl的enqueueDiskWrite方法來執行數據寫入工作,注意這裏的第二個參數,如果不爲null,則代表異步寫入。
我們再來看commit()方法。
EditorImpl的commit方法
@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;
}
邏輯解析:
- commit方法同樣先調用了commitToMemory()來把數據更新至內存中。
- 然後調用SharedPreferencesImpl的enqueueDiskWrite方法來執行數據寫入工作,注意這裏是同步寫入,第二個參數爲null。
apply和commit方法都調用了兩個關鍵方法:commitToMemory和SharedPreferencesImpl的enqueueDiskWrite方法,我們逐個分析。
EditorImpl的commitToMemory方法
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
if (mDiskWritesInFlight > 0) {
mMap = new HashMap<String, Object>(mMap); //如果其他線程也正在進行寫入操作,我們先把mMap的鍵值對複製出一份。
}
mapToWriteToDisk = mMap; //直接在mapToWriteToDisk上進行操作
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) {//如果調用了Editor的clear,則先將map中的數據進行清除。
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
mapToWriteToDisk.clear();
}
mClear = false;
}
for (Map.Entry<String, Object> e : mModified.entrySet()) { //遍歷mModified中的數據
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)) {//key和value都是空值,則跳過該條數據
continue;
}
mapToWriteToDisk.remove(k);//key值存在,value爲null,則將數據刪除。
} else {
if (mapToWriteToDisk.containsKey(k)) {
Object existingValue = mapToWriteToDisk.get(k);
if (existingValue != null && existingValue.equals(v)) {//key在map中已經存在,並且value沒有改變,則跳過
continue;
}
}
mapToWriteToDisk.put(k, v);//將key/value寫入map中
}
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);//返回結果
}
邏輯解析:
- mMap中存儲的是該sp文件的所有數據,是sp文件在內存中的映射。
- 判斷其他線程是否也正在進行寫入操作,如果是,則把mMap的鍵值對複製出一份。
- 將mMap賦值給mapToWriteToDisk變量,後面直接在mapToWriteToDisk上進行操作。
- 如果調用了Editor的clear,則將map中的數據進行清除。
- 遍歷mModified中的數據,mModified保存了本次事務提交的所有修改,上文中的putInt的數據,就存在該Map中。
- 判斷,key和value都是空值,則跳過該條數據。
- key值存在,value爲null,則將數據刪除。
- key在map中已經存在,並且value沒有改變,則跳過。
- 最後將key和value寫入mapToWriteToDisk中。
- 如果數據已經改變,則設置changesMade變量爲true。
- 最後返回已修改的內存數據對象MemoryCommitResult。
SharedPreferencesImpl的enqueueDiskWrite方法
數據在內存中更新之後,最後一步就是寫入存儲分區了,我們來看它對應的方法。
SharedPreferencesImpl的enqueueDiskWrite方法:
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);
}
邏輯解析:
首先判斷是否需要同步,這裏可以看到apply是異步的,commit是同步的(有條件的同步)。
如果只有一個線程在執行寫入操作mDiskWritesInFlight的值是1,則直接調用writeToFile方法執行寫入工作。
否則,調用QueuedWork.queue方法添加到任務隊列中執行,等待執行。
這裏commit同步提交也是有條件的,如果commit時,該sp文件正在被其他線程執行數據寫入,則執行異步寫入。
Queued類的queue方法
我們來看異步寫入的執行:
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);//如果是applay提交,這裏直接delay了100毫秒再執行
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
異步任務是通過HandlerThread來實現的,我們來看它的初始化過程:
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;
}
}
這裏創建了一個HandlerThread來執行異步,也就是任務隊列是單線程的,並且線程優先級是前臺線程優先級。
SharedPreferencesImpl的writeToFile方法
寫入存儲分區真正的執行方法是SharedPreferencesImpl的writeToFile方法:
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
……
boolean fileExists = mFile.exists(); //sp文件是否存在
……
if (fileExists) {
boolean needsWrite = false; //本次是否需要執行寫入操作
// Only need to write if the disk state is older than this commit
if (mDiskStateGeneration < mcr.memoryStateGeneration) { //mDiskStateGeneration是一個long類型的值,記錄着最後一次數據提交的狀態值。
if (isFromSyncCommit) {//如果是同步寫入操作,這裏指commit提交的,直接執行寫入存儲分區操作
needsWrite = true;
} else {
synchronized (mLock) {
// No need to persist intermediate states. Just wait for the latest state to
// be persisted.
if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) { //如果是異步寫入,這裏指apply提交的,則判斷是否是最新的一次寫入請求,如果不是,則不執行寫入存儲分區操作,以優化性能。
needsWrite = true;
}
}
}
}
if (!needsWrite) { //不需要執行數據寫入
mcr.setDiskWriteResult(false, true);
return;
}
boolean backupFileExists = mBackupFile.exists();//判斷備份是否存在
if (DEBUG) {
backupExistsTime = System.currentTimeMillis();
}
if (!backupFileExists) {//如果備份不存在,則把sp原始文件重命名爲備份文件。
if (!mFile.renameTo(mBackupFile)) {
Log.e(TAG, "Couldn't rename file " + mFile
+ " to backup file " + mBackupFile);
mcr.setDiskWriteResult(false, false);
return;
}
} else {//備份存在,則將file文件刪除,因爲它可能是錯誤數據。
mFile.delete();
}
}
//到了這裏,sp對應的原始文件已經被刪除了,只存在備份文件了。
// Attempt to write the file, delete the backup and return true as atomically as
// possible. If any exception occurs, delete the new file; next time we will restore
// from the backup.
try {
FileOutputStream str = createFileOutputStream(mFile);//創建一個空的原始文件,以存儲sp數據。
if (DEBUG) {
outputStreamCreateTime = System.currentTimeMillis();
}
if (str == null) {
mcr.setDiskWriteResult(false, false);
return;
}
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);//執行xml數據解析,將內存中的key-value鍵值對存儲到str的數據流中。
writeTime = System.currentTimeMillis();
FileUtils.sync(str);//將數據流寫入到存儲分區中。
fsyncTime = System.currentTimeMillis();
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
……
mBackupFile.delete();//寫入完成後,將備份文件刪除。
……
mDiskStateGeneration = mcr.memoryStateGeneration;//更新mDiskStateGeneration的值,代表了最後一次寫入時的值
mcr.setDiskWriteResult(true, true);
……
return;
} catch (XmlPullParserException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
} catch (IOException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
}
// 如果磁盤寫入失敗,則刪除原始sp文件。
if (mFile.exists()) {
if (!mFile.delete()) {
Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
}
}
mcr.setDiskWriteResult(false, false);
}
邏輯解析:
sp文件寫入存儲分區(磁盤)的工作是由SharedPreferencesImpl的writeToFile方法來完成的,邏輯較多,根據代碼邏輯,我們簡單總結一些它的過程:
- 首先使用fileExists變量來判斷sp文件是否存在。
- 如果存在,判斷本次是否需要執行寫入操作,用變量needsWrite表示。mDiskStateGeneration是一個long類型的值,記錄着最後一次數據提交的狀態值,用來判斷是否存在更新。
- 如果是commit提交的,則needsWrite賦值爲true,執行數據寫入。
- 如果是apply提交的,則判斷是否是最新的一次寫入請求如果是,則needsWrite賦值爲true,如果不是(是一箇中間提交值,之後還有提交),則不執行寫入存儲分區操作,以優化性能。
- 如果needsWrite的值爲false則不需要執行數據寫入,結束本次任務。
- 接下來判斷sp文件備份是否存在,如果備份不存在,則把sp原始文件重命名爲備份文件。如果備份存在,則將file文件刪除,因爲它可能是錯誤數據。
- 2~6是sp文件存在時的處理邏輯。到了這裏,sp對應的原始文件已經被刪除了,只存在備份文件了(如果存在的話)。
- 接下來執行寫入存儲分區,首先創建一個空文件,以存儲sp數據。
- 執行xml數據解析,將內存中的key-value鍵值對存儲到str的數據流中。
- 將數據流寫入到存儲分區中。
- 寫入完成後,將備份文件刪除。
- 更新mDiskStateGeneration的值,代表了最後一次寫入時的狀態值。
- 最後,如果磁盤寫入失敗,則刪除原始sp文件。
好了,到了這裏,SharedPreferences的實現原理我們也就分析完了,那麼在使用過程時,你是否也瞭解了SharedPreferences的正確打開方式呢?
SharedPreferences性能問題及最佳實踐
sp文件的io操作
- sp文件存儲在“/data/data/<app包名>/shared_prefs/”目錄下,存儲格式是以.xml文件的形式存在。
- sp文件在創建SharedPreferencesImpl對象時,會把文件從磁盤分區加載到內存中,並且存儲到Map中。
- sp文件讀取是在子線程中進行的,子線程的優先級等同於它的父線程優先級。
- sp文件在更新時,首先會更新到內存的Map對象中,根據提交的方式,commit通常會強制、同步寫入,apply會執行異步寫入工作,並且會等待100ms。
- sp在異步提交時,使用的是ThreadHandler,線程優先級爲前臺任務的單線程操作,如果任務很多,怎會等待。
- sp文件在寫入時,會刪除舊文件,新建新的文件,重新執行寫入。
- sp文件的寫入更新方式是,全文件內容替換。
- Context.MODE_MULTI_PROCESS模式時,每次調用getSharedPreferences獲取SharedPreferences對象時,都會檢查數據是否更新,如果更新,則從磁盤重新加載文件到內存中。
SharedPreferences的性能及最佳實踐
sp的性能問題:
- sp文件存儲在App私有目錄,所以會隨着App卸載而刪除。
- sp文件存儲的數據格式是.xml,每次從磁盤讀取和寫入操作,都需要解析xml,效率不高。
- sp的大量使用會佔用大量的內存,因爲它會把所有用到的sp文件內容都同步到內存中。
- sp錯誤使用,會導致大量的io操作,影響系統性能,例如,頻繁的commit或apply。
- Context.MODE_MULTI_PROCESS模式的誤用,會產生大量的io操作,嚴重影響性能。
- sp是在新建線程執行初始化工作,如果App啓動時,在主線程執行大量的sp初始化工作,會創建大量的線程,且線程優先級同UI線程,這樣會造成sp線程搶佔UI線程資源,造成啓動過慢等問題。
- sp如果是在優先級較低子線程中執行sp的初始化工作,則sp加載過程可能會變的很長。
- sp在提交時,如果在ui線程中使用commit同步提交,則可能會導致因等待而產生的ANR問題。
- sp每次更新到磁盤都是整體寫入,性能影響較大。
- sp在執行數據寫入時,都會創建EditorImpl對象,大量的提交操作會創建大量的EditorImpl對象,佔用大量內存。
- sp跨進程訪問模式,不可靠,已廢棄。
- 當單個sp文件大於50k時(經驗值,不同機器差別較大),io會變的非常緩慢。
- sp文件在執行apply寫入時,至少要等到100ms以上。
sp的最佳實踐
- 推薦使用sp存儲一些數據量較小的應用配置類信息。
- 不要使用sp的Context.MODE_MULTI_PROCESS模式;不要指望使用sp來進行跨進程數據操作。
- 單個sp文件大小最好保持在10kb以內,最大不要超過50kb。
- 將不同的業務數據保存在不同的sp文件內,不要一個文件存儲所有數據。
- sp數據更新時,最好多次修改後,統一執行一次commit或apply,以減少io次數。
- sp文件數量也要進行控制,以減少線程數量和內存使用。
- ui線程中使用sp數據要注意時效性,最好在使用之前,預加載到內存。
- sp加載時,會在子線程執行,子線程的優先級等同於父線程,一定要注意加載的時間。