0. 前言
SharedPreferences可以說是Android中最常用的一種存數據到文件的方式。他的數據是以鍵值對的方式存儲在 ~/data/data/包名/shared_prefs
這個文件夾中的。
這個存儲框架是非常輕量級的,如果我們需要存一些小數據或者是一個小型的可序列化的Bean實體類的,使用SharedPreferences是最明智的選擇。
1. 使用方法
1.1 獲取SharedPreferences
在使用SharedPreferences前,我們得先獲取到它。
由於SharedPreferences是Android內置的一個框架,所以我們想要獲取到它非常的簡單,不需要導入任何依賴,直接寫代碼就行。下面我們就來介紹下獲取對象的三個方式:
1.1.1 Context # getSharedPreferences()
首先就是可以說是最常用的方法,通過Context的getSharedPreferences()
方法去獲取到SharedPreferences對象。由於是通過Context獲取的,所以基本上Android的所有場景我們都可以通過這個方法獲取到。
public abstract SharedPreferences getSharedPreferences (String name,
int mode)
這個方法接收兩個參數,分別是name
和mode
:
name
:name就是我們要存儲的SharedPreferences本地文件的名字,這個可以自定義。但是如果使用同樣的name的話,永遠只能獲取到同一個SharedPreferences的對象。mode
:mode就是我們要獲取的這個SharedPreferences的訪問模式,Android給我們提供了挺多的模式的,但是由於其餘的模式或多或少存在着安全隱患(因爲其他應用也可以直接獲取到),所以就全部都棄用了,現在就只有一個MODE_PRIVATE
模式。
此外,這個方法是線程安全的。
Mode
的可選參數:
MODE_PRIVATE
:私有模式,該SharedPreferences只會被調用他的APP去使用,其他的APP無法獲取到這個SharedPreferences。:API17被棄用。使用這個模式,所有的APP都可以對這個SharedPreferences進行讀操作。所以這個模式被Android官方嚴厲警告禁止使用(It is strongly discouraged),並推薦使用MODE_WORLD_READABLE
ContentProvider
、BroadcastReceiver
和Service
。:API17被棄用。和上面類似,這個是可以被所有APP進行寫操作。同樣也是被嚴厲警告禁止使用。MODE_WORLD_WRITEABLE
:API23被棄用。使用了這個模式,允許多個進程對同一個SharedPreferences進行操作,但是後來也被啓用了,原因是因爲在某些Android版本下,這個模式不能可靠的運行,官方建議如果多進程建議使用MODE_MULTI_PROCESS
ContentProvider
去操作。在後面我們會說爲啥多進程下不可靠。
1.1.2 Activity # getPreferences()
這個方法只能在Activity中或者通過Activity對象去使用。
public SharedPreferences getPreferences (int mode)
這個方法需要傳入一個mode
參數,這個參數和上面的context#getSharedPreferences()
的mode
參數是一樣的。其實這個方法和上面Context的那個方法是一樣的,他兩都是調用的SharedPreferences getSharedPreferences(String name, int mode)
。只不過Context的需要你去指定文件名,而這個方法你不需要手動去指定,而是會自動將當前Activity的類名作爲了文件名。
1.1.3 PreferencesManager # getDefaultSharedPreferences()
這個一般用在Android的設置頁面上,或者說,我們也只有在構建設置頁面的時候纔會去使用這個。
public static SharedPreferences getDefaultSharedPreferences (Context context)
他承接一個context參數,並自動將當前應用的報名作爲前綴來命名文件。
1.2 存數據
如果需要往SharedPreferences中存儲數據的話,我們並不能直接對SharedPreferences對象進行操作,因爲SharedPreferences沒有提供存儲或者修改數據的接口。
如果想要對SharedPreferences存儲的數據進行修改,需要通過SharedPreferences.edit()
方法去獲取到SharedPreferences.Editor對象來進行操作。
獲取到Editor對象後,我們就可以調用他的putXXX()
方法進行存儲了,存儲之後一定記得通過apply()
和commit()
方法去將數據提交。
至於commit
和apply
的區別我們後面會說。
//步驟1:創建一個SharedPreferences對象
SharedPreferences sharedPreferences= getSharedPreferences("data",Context.MODE_PRIVATE);
//步驟2: 實例化SharedPreferences.Editor對象
SharedPreferences.Editor editor = sharedPreferences.edit();
//步驟3:將獲取過來的值放入文件
editor.putString("name", “Tom”);
editor.putInt("age", 28);
editor.putBoolean("marrid",false);
//步驟4:提交
editor.commit();
// 刪除指定數據
editor.remove("name");
editor.commit();
// 清空數據
editor.clear();
editor.commit();
1.3 取數據
取值就很簡單了,構建出SharedPreferences的對象後,就直接調用SharedPreferences的getXXX()
方法就行。
SharedPreferences sharedPreferences = getSharedPreferences("data", Context .MODE_PRIVATE);
String userId = sharedPreferences.getString("name", "");
2. 源碼分析
2.1 獲取SharedPreferences實例
我們上面說到,獲取SharedPreferences實例最常用的方法就是Context#getSharedPreferences()
。那我們就從這個方法入手,看到底是怎麼獲取到SharedPreferences實例的。
我們先看下這個方法的實現:
public class ContextWrapper extends Context {
@UnsupportedAppUsage
Context mBase;
// ...
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
return mBase.getSharedPreferences(name, mode);
}
}
可以看到他又調用了Context的getSharedPreferences()
方法:
public abstract SharedPreferences getSharedPreferences(String name, @PreferencesMode int mode);
然後我們就會驚喜的發現,這是一個抽象方法。我開始還想去找一個ContextWrapper
的構造的地方,看看mBase
傳入的是啥,後來找了一圈沒找到,直接上網搜索,立馬得到答案:ContextImpl
,這個可以說是Context
在Android中的唯一實現類,所有的操作又得經過這個類。那麼我們就來看下這個類中的getSharedPreferences()
方法的實現:
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
// 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.
// ps:這個nice很精髓😂
if (mPackageInfo.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
if (name == null) {
name = "null";
}
}
File file;
// 加了一個類鎖,保證同步
synchronized (ContextImpl.class) {
// mSharedPrefsPaths是一個保存了name和file對應關係的ArrayMap
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
// 根據name從裏面找有沒有緩存的file
file = mSharedPrefsPaths.get(name);
// 如果沒有,那就調用getSharedPreferencesPath去找
if (file == null) {
// ->>> 重點1. getSharedPreferencesPath(name)
file = getSharedPreferencesPath(name);
// 並保存到mSharedPrefsPaths
mSharedPrefsPaths.put(name, file);
}
}
// 獲取到file後,再調用getSharedPreferences
return getSharedPreferences(file, mode);
}
/**
* 重點1. ContextImpl # getSharedPreferencesPath(String name)
* 根據PreferencesDir和name.xml去創建了這個文件
*/
@Override
public File getSharedPreferencesPath(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
那我們在看下getSharedPreferences(File file, int mode)
的實現:
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
// SharedPreferences唯一實現類SharedPreferencesImpl的實例
SharedPreferencesImpl sp;
// 同樣的加類鎖
synchronized (ContextImpl.class) {
// 構造了一個File-SharedPreferencesImpl對應關係的ArrayMap
// 調用getSharedPreferencesCacheLocked方法區獲取cahce
// ->>> 重點1
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
// 從file-SharedPreferencesImpl鍵值對中根據當前file去過去SharedPreferencesImpl實例
sp = cache.get(file);
// 如果沒有,那就需要新建一個
if (sp == null) {
// 檢查mode,如果是MODE_WORLD_WRITEABLE或者MODE_MULTI_PROCESS則直接拋異常
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");
}
}
// 調用構造方法去構造SharedPreferencesImpl對象
sp = new SharedPreferencesImpl(file, mode);
// 將對象和file的鍵值對存入cache中
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;
}
/**
* 重點1. ContextImap # getSharedPreferencesCacheLocked()
* 根據當前的包名,去獲取到由此應用創建的File-SharedPreferencesImpl的Map對象,
* 而這個對象裏面就存放了這個應用創建的所有的SharedPreferencesImpl和File的對應關係
*/
@GuardedBy("ContextImpl.class")
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
// 如果sSharedPrefsCache爲空就構造一個ArrayMap
// sSharedPrefsCache就是一個存放String-String, ArrayMap<File, SharedPreferencesImpl>的Map
// 換句話說,也就是存放包名-packagePrefs對應關係的Map
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>();
}
// 獲取包名
final String packageName = getPackageName();
// 到sSharedPrefsCache中找
ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
// 如果找不到,就構建一個然後存進去
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}
// 找得到就返回
return packagePrefs;
}
2.2 構建SharedPreferencesImpl
2.2.1 SharedPreferencesImpl構造方法
我們先來看下這個類的構造方法:
@UnsupportedAppUsage
SharedPreferencesImpl(File file, int mode) {
mFile = file;
// file的備份文件
mBackupFile = makeBackupFile(file);
mMode = mode;
// 從磁盤加載的標誌,當需要從磁盤加載時將其設爲true,這樣如果有其他線程也調用了SharedPreferences的加載方法時,就會因爲其爲true而直接返回也就不執行加載方法
// 保證了全局只有一個線程在加載
mLoaded = false;
// SharedPreferences中的數據
mMap = null;
// 保存的錯誤信息
mThrowable = null;
startLoadFromDisk();
}
初始化參數後立馬調用了startLoadFromDisk()
方法:
2.2.2 startLoadFromDisk()
@UnsupportedAppUsage
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
// 開啓一個新線程來加載數據
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
2.2.3 loadFromDIsk()
loadFromDisk()
:
private void loadFromDisk() {
synchronized (mLock) {
// 如果已經家在過了,就直接退出
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<String, Object> map = null;
// 文件信息,對應的是C語言stat.h中的struct stat
StructStat stat = null;
Throwable thrown = null;
try {
// 通過文件路徑去構建StructStat對象
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
// 從XML中把數據讀出來,並把數據轉化成Map類型
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16 * 1024);
map = (Map<String, Object>) XmlUtils.readMapXml(str);
} 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();
}
}
}
到目前來說,就完成的SharedPreferencesImpl的構建過程。
2.3 讀數據 SharedPreferences # getXXX()
相對來說,讀數據涉及到的方法比寫數據簡單得多,所以我們先來看下讀數據:
我們以getString()
爲例
2.3.1 getString
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
// 見2.3.2
awaitLoadedLocked();
// 從map中獲取數據
String v = (String)mMap.get(key);
// 如果獲取到數據,就返回數據,否則返回方法參數中給定的默認值
return v != null ? v : defValue;
}
}
2.3.2 awaitLoadedLocked
@GuardedBy("mLock")
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 {
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
這個方法簡單點來說就是如果mLoad
不爲true也就是沒有加載完成的話,就等待加載完成。
2.4 寫數據
2.4.1 SharedPreferences.Editor
但是光構建了對象還不夠,我們還得能對她進行操作。我們前面說到過,SharedPreferences並不提供修改的功能,如果你想對她進行修改,必須通過SharedPreferences.Editor
來實現。
我們來看下SharedPreferences.edit()
:
@Override
public Editor edit() {
// TODO: remove the need to call awaitLoadedLocked() when
// requesting an editor. will require some work on the
// Editor, but then we should be able to do:
//
// context.getSharedPreferences(..).edit().putString(..).apply()
//
// ... all without blocking.
synchronized (mLock) {
// ->>> 重點1
awaitLoadedLocked();
}
// 創建了一個EditorImpl的對象,
// 但是這塊需要注意下,我們想對SharedPreferences進行修改,就必須調用edit()方法,就會去構建一個新的EditorImpl對象
// 所以爲了避免不必要的開銷,我們在使用時最好一次性完成對數據的操作
return new EditorImpl();
}
/**
* 重點1:SharedPreferencesImpl # awaitLoadedLocked()
*/
@GuardedBy("mLock")
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 {
// 如果還沒有加載完成,就進入等待狀態
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
2.4.2 EditorImpl
2.4.2.1 putXXX()
那我們再來看下putXXX()
方法,我們以putString()
來舉例:
public final class EditorImpl implements Editor {
private final Object mEditorLock = new Object();
// 存數據的HashMap
@GuardedBy("mEditorLock")
private final Map<String, Object> mModified = new HashMap<>();
@GuardedBy("mEditorLock")
private boolean mClear = false;
@Override
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
putString()
方法很簡單,直接將數據put到存數據的HashMap中去就行了。或者說,所有的putXXX()
都是這麼簡單。
但是,如果我們想將修改提交到SharedPreferences裏面去的話,還需要調用apply()
或者commit()
方法,那我們現在來看下這兩個方法。
2.4.2.2 apply()
@Override
public void apply() {
// 獲取當前時間
final long startTime = System.currentTimeMillis();
// 見2.2.3.4
// 構建了一個MemoryCommitResult的對象
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");
}
}
};
// 將awaitCommit添加到Queue的Word中去
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
// 執行操作,並從QueuedWord中刪除
awaitCommit.run();
QueuedWork.removeFinisher(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);
}
2.4.2.3 commit()
@Override
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
// 見2.2.3.4
// 構建了一個MemoryCommitResult對象
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");
}
}
// 通知監聽則, 並在主線程回調onSharedPreferenceChanged()方法
notifyListeners(mcr);
// 返回文件操作的結果數據
return mcr.writeToDiskResult;
}
2.4.2.4 commitToMemory()
// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
// 當前Memory的狀態,其實也就是當需要提交數據到內存的時候,他的值就加一
long memoryStateGeneration;
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
// 存數據的Map
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
// 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);
}
mapToWriteToDisk = mMap;
// 2.2.3.5的關鍵點
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
keysModified = new ArrayList<String>();
listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
synchronized (mEditorLock) {
boolean changesMade = false;
// 如果mClear爲true,就清空mapToWriteToDisk
if (mClear) {
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
mapToWriteToDisk.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 (!mapToWriteToDisk.containsKey(k)) {
continue;
}
mapToWriteToDisk.remove(k);
} else {
if (mapToWriteToDisk.containsKey(k)) {
Object existingValue = mapToWriteToDisk.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mapToWriteToDisk.put(k, v);
}
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
這段代碼剛開始看的時候有點暈,但是看完後就瞬間懂了,這段代碼主要執行了一下的功能:
- 將
mMap
賦值給mapToWriteToDisk
- 當
mClear
爲true的時候,清空mapToWriteToDisk
- 遍歷
mModified
,mModified
也就是我們上面說到的保存本次edit的數據的HashMap- 噹噹前的
value
爲null或者this
的時候,移除對應的k
- 噹噹前的
- 構建了一個
MemoryCommitResult
對象
2.4.2.5 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) {
// 這個方法這塊就不講了,太長了,大家感興趣可以看下
// 主要功能就是
// 1. 當沒有key沒有改變,則直接返回了;否則執行下一步
// 2. 將mMap全部信息寫入文件,如果寫入成功則刪除備份文件,如果寫入失敗則刪除mFile
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
// 當寫入成功後,將標誌位減1
mDiskWritesInFlight--;
}
// 此時postWriteRunnable爲null不執行該方法
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
// 如果是commit則進入
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
// 由於commitToMemory會讓mDiskWritesInFlight+1,則wasEmpty爲true
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
// 在執行一遍上面的操作,保證將commit的內容也保存
writeToDiskRunnable.run();
return;
}
}
// 如果是apply()方法,則會將任務放入單線程的線程池中去執行
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
所以從這個方法我們可以看到:
commit()
是直接同步執行的,有數據就存入磁盤apply()
是先將awaitCommit
放入QueuedWork
,然後在單線程的線程池中去執行,執行完畢後再將awaitCommit
從QeueudWork
中移除。
3. 知識點
3.1 apply和commit的區別
- apply沒有返回值, commit有返回值能知道修改是否提交成功
- apply是將修改提交到內存,再異步提交到磁盤文件; commit是同步的提交到磁盤文件;
- 多併發的提交commit時,需等待正在處理的commit數據更新到磁盤文件後纔會繼續往下執行,從而降低效率; 而apply只是原子更新到內存,後調用apply函數會直接覆蓋前面內存數據,從一定程度上提高很多效率。
3.2 多進程的問題
我們前面說到了,SP提供了多進程訪問,雖說沒有像World模式那樣會直接拋異常,但是官方不建議多進程下使用SP。
那麼我們不禁會好奇,多進程下訪問SP會有什麼問題呢?
探究這個問題,我們得先回到ContextImpl#getSharedPreferences(File file, int mode)
方法:
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
// ...前面的代碼省略的,如果大家想回憶下,可以跳轉到2.1節
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.
// ->>> 重點1
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
/**
* 重點1 SharedPreferencesImpl # startReloadIfChangedUnexpectedly()
*/
void startReloadIfChangedUnexpectedly() {
synchronized (mLock) {
// TODO: wait for any pending writes to disk?
// ->>> 重點2
if (!hasFileChangedUnexpectedly()) {
return;
}
// ->>> 重點3
startLoadFromDisk();
}
}
/**
* 重點2 SharedPreferencesImpl # hasFileChangedUnexpectedly()
* 如果文件發生了預期之外的修改,也就是說有其他進程在修改,就返回true,否則false
*/
private boolean hasFileChangedUnexpectedly() {
synchronized (mLock) {
// 如果mDiskWritesInFlight大於0,就證明是在當前進程中修改的,那就不用重新讀取
if (mDiskWritesInFlight > 0) {
// If we know we caused it, it's not unexpected.
if (DEBUG) Log.d(TAG, "disk write in flight, not unexpected.");
return false;
}
}
final StructStat stat;
try {
/*
* Metadata operations don't usually count as a block guard
* violation, but we explicitly want this one.
*/
BlockGuard.getThreadPolicy().onReadFromDisk();
stat = Os.stat(mFile.getPath());
} catch (ErrnoException e) {
return true;
}
synchronized (mLock) {
return !stat.st_mtim.equals(mStatTimestamp) || mStatSize != stat.st_size;
}
}
/**
* 重點3 SharedPreferencesImpl # startLoadFromDisk()
*/
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
// ->>> 重點4,這塊代碼可以回到2.2.3看一下
loadFromDisk();
}
}.start();
}
我們可以看到:每次獲取SharedPreferences實例的時候嘗試從磁盤中加載數據,並且是在異步線程中,因此一個線程的修改最終會反映到另一個線程,但不能立即反映到另一個進程,所以通過SharedPreferences無法實現多進程同步。
loadFromDisk()
方法中我們最需要關注的是這一段:
// 如果備份文件已經存在,那就刪除源文件,並將備份文件替換爲源文件
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
這塊判斷了mBackupFile
是否存在,那mBackupFile
我們是在哪創建的呢?
整個SharedPreferencesImpl中有兩處:
- 構造方法:會調用
makeBackupFile()
給傳入的file
構造一個mBackupFile
- writeToFile():在寫入到磁盤的文件時,如果沒有
mBackupFile
,就會根據當前的mFile
重命名爲mBackupFile
而writeToFile()
在enqueueDiskWrite()
中被調用,這個方法太長了,我截取下關鍵信息:
@GuardedBy("mWritingToDiskLock")
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
// ...
boolean fileExists = mFile.exists();
// ...
// Rename the current file so it may be used as a backup during the next read
if (fileExists) {
// ...
boolean backupFileExists = mBackupFile.exists();
// ...
if (!backupFileExists) {
if (!mFile.renameTo(mBackupFile)) {
Log.e(TAG, "Couldn't rename file " + mFile
+ " to backup file " + mBackupFile);
mcr.setDiskWriteResult(false, false);
return;
}
} else {
mFile.delete();
}
}
// 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);
// ...
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
writeTime = System.currentTimeMillis();
FileUtils.sync(str);
fsyncTime = System.currentTimeMillis();
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
// ...
try {
final StructStat stat = Os.stat(mFile.getPath());
synchronized (mLock) {
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
}
} catch (ErrnoException e) {
// Do nothing
}
if (DEBUG) {
fstatTime = System.currentTimeMillis();
}
// Writing was successful, delete the backup file if there is one.
mBackupFile.delete();
// ...
所以我們大致總結下這個方法的功能:
- 如果源文件
mFIle
存在並且備份文件mBackupFile
不存在,就將源文件重命名爲備份文件,如果源文件存在並且備份文件存在,就刪除源文件 - 重新創建源文件
mFile
,並將內容寫進去 - 刪除
mBackupFile
結合一下loadFromDisk()
和writeToFile()
兩個方法,我們可以推測出:當存在兩個進程,一個讀進程,一個寫進程,由於只有在創建SharedPreferencesImpl
的時候創建了一個備份進程,此時讀進程會將源文件刪除,並將備份文件重命名爲源文件,這樣的結果就是,讀進程永遠只會看到寫之前的內容。並且由於寫文件需要調用createFileOutputStream(mFile)
,但是這個時候由於源文件被讀進程刪除了,所以導致寫進程的mFIle
沒有了引用,也就會創建失敗,導致修改的數據無法更新到文件上,進而導致數據丟失。
3.3 建議優化
- 不要在SP中存儲較大的key或者value
- 只是用MODE_PRIVATE模式,其它模式都不要使用(也被棄用了)
- 可以的話,儘量獲取一次Editor然後提交所有的數據
- 不要高頻使用apply,因爲他每次都會新建一個線程;使用commit的時需謹慎,因爲他在主線程中操作(對,就是主線程,主線程並不是只能更新UI,但是還是就把主線程當做更新UI的爲好,我們的耗時操作最好不要在主線程中)
- 如果需要在多進程中存儲數據,建議使用ContentProvider