SharedPreferences
應(yīng)該是任何一名 Android 初學(xué)者都知道的存儲(chǔ)類了,它輕量游昼,適合用于保存軟件配置等參數(shù)凿掂。以鍵值對(duì)的 XML 文件形式存儲(chǔ)在本地,程序卸載后也會(huì)一并清除顶考,不會(huì)殘留信息。
使用起來(lái)也非常簡(jiǎn)單妖泄。
// 讀取
val sharedPreferences = getSharedPreferences("123", Context.MODE_PRIVATE)
val string = sharedPreferences.getString("123","")
// 寫(xiě)入
val editor = sharedPreferences.edit()
editor.putString("123","123")
editor.commit()
當(dāng)我們寫(xiě)下這樣的代碼的時(shí)候驹沿,IDE 極易出現(xiàn)一個(gè)警告,提示我們用 apply()
來(lái)替換 commit()
蹈胡。原因也很簡(jiǎn)單渊季,因?yàn)?commit()
是同步的朋蔫,而 apply()
采用異步的方式通常來(lái)說(shuō)效率會(huì)更高一些。但是却汉,當(dāng)我們把 editor.commit()
的返回值賦給一個(gè)變量的時(shí)候驯妄,這時(shí)候就會(huì)發(fā)現(xiàn) IDE 沒(méi)有了警告。這是因?yàn)?IDE 認(rèn)為我們想要使用 editor.commit()
的返回值了合砂,所以青扔,通常來(lái)說(shuō),在我們不關(guān)心操作結(jié)果的時(shí)候翩伪,我們更傾向于使用 apply()
進(jìn)行寫(xiě)入的操作微猖。
獲取 SharedPreferences 實(shí)例
我們可以通過(guò) 3 種方式來(lái)獲取 SharedPreferences
的實(shí)例。
首先當(dāng)然是我們最常見(jiàn)的寫(xiě)法缘屹。
getSharedPreferences("123", Context.MODE_PRIVATE)
Context
的任意子類都可以直接通過(guò) getSharedPreferences()
方法獲取到 SharedPreferences
的實(shí)例凛剥,接受兩個(gè)參數(shù),分別對(duì)應(yīng) XML 文件的名字和操作模式轻姿。其中 MODE_WORLD_READABLE
和 MODE_WORLD_WRITEABLE
這兩種模式已在 Android 4.2 版本中被廢棄犁珠。
- Context.MODE_PRIVATE: 指定該
SharedPreferences
數(shù)據(jù)只能被本應(yīng)用程序讀、寫(xiě)互亮; - Context.MODE_WORLD_READABLE: 指定該
SharedPreferences
數(shù)據(jù)能被其他應(yīng)用程序讀犁享,但不能寫(xiě); - Context.MODE_WORLD_WRITEABLE: 指定該
SharedPreferences
數(shù)據(jù)能被其他應(yīng)用程序讀豹休; - Context.MODE_APPEND:該模式會(huì)檢查文件是否存在炊昆,存在就往文件追加內(nèi)容,否則就創(chuàng)建新文件慕爬;
另外在 Activity
的實(shí)現(xiàn)中,還可以直接通過(guò) getPreferences()
獲取屏积,實(shí)際上也就把當(dāng)前 Activity 的類名作為文件名參數(shù)医窿。
public SharedPreferences getPreferences(@Context.PreferencesMode int mode) {
return getSharedPreferences(getLocalClassName(), mode);
}
此外,我們也可以通過(guò) PreferenceManager
的 getDefaultSharedPreferences()
獲取到炊林。
public static SharedPreferences getDefaultSharedPreferences(Context context) {
return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
getDefaultSharedPreferencesMode());
}
public static String getDefaultSharedPreferencesName(Context context) {
return context.getPackageName() + "_preferences";
}
private static int getDefaultSharedPreferencesMode() {
return Context.MODE_PRIVATE;
}
可以很明顯的看到姥卢,這個(gè)方式就是在直接把當(dāng)前應(yīng)用的包名作為前綴來(lái)進(jìn)行命名的。
注意:如果在 Fragment 中使用
SharedPreferences
時(shí)渣聚,SharedPreferences
的初始化盡量放在onAttach(Activity activity)
里面進(jìn)行 独榴,否則可能會(huì)報(bào)空指針,即getActivity()
會(huì)可能返回為空奕枝。
SharedPreferences 源碼(基于 API 28)
有較多 SharedPreferences
使用經(jīng)驗(yàn)的人棺榔,就會(huì)發(fā)現(xiàn) SharedPreferences
其實(shí)具備挺多的坑,但這些坑主要都是因?yàn)椴皇煜て渲姓嬲脑硭鶎?dǎo)致的隘道,所以症歇,筆者在這里郎笆,帶大家一起揭開(kāi) SharedPreferences
的神秘面紗。
SharedPreferences 實(shí)例獲取
前面講了 SharedPreferences
有三種獲取實(shí)例的方法忘晤,但歸根結(jié)底都是調(diào)用的 Context
的 getSharedPreferences()
方法宛蚓。由于 Android 的 Context
類采用的是裝飾者模式,而裝飾者對(duì)象其實(shí)就是 ContextImpl
设塔,所以我們來(lái)看看源碼是怎么實(shí)現(xiàn)的凄吏。
// 存放的是名稱和文件夾的映射,實(shí)際上這個(gè)名稱就是我們外面?zhèn)鬟M(jìn)來(lái)的 name
private ArrayMap<String, File> mSharedPrefsPaths;
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.
if (mPackageInfo.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
if (name == null) {
name = "null";
}
}
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);
}
@Override
public File getSharedPreferencesPath(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
private File makeFilename(File base, String name) {
if (name.indexOf(File.separatorChar) < 0) {
return new File(base, name);
}
throw new IllegalArgumentException(
"File " + name + " contains a path separator");
}
可以很明顯的看到闰蛔,內(nèi)部是采用 ArrayMap
來(lái)做的處理痕钢,而這個(gè) mSharedPrefsPaths
主要是用于存放名稱和文件夾的映射,實(shí)際上這個(gè)名稱就是我們外面?zhèn)鬟M(jìn)來(lái)的 name钞护,這時(shí)候我們通過(guò) name 拿到我們的 File盖喷,如果當(dāng)前池子中沒(méi)有的話,則直接新建一個(gè) File难咕,并放入到 mSharedPrefsPaths
中课梳。最后還是調(diào)用的重載方法 getSharedPreferences(File,mode)
// 存放包名與ArrayMap鍵值對(duì),初始化時(shí)會(huì)默認(rèn)以包名作為鍵值對(duì)中的 Key余佃,注意這是個(gè) static 變量
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
@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;
}
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;
}
可以看到暮刃,又采用了一個(gè) ArrayMap
來(lái)存放文件和 SharedPreferencesImpl
組成的鍵值對(duì),然后通過(guò)通過(guò)單例的方式返回一個(gè) SharedPreferences
對(duì)象爆土,實(shí)際上是 SharedPreferences
的實(shí)現(xiàn)類 SharedPreferencesImpl
椭懊,而且在其中還建立了一個(gè)內(nèi)部緩存機(jī)制。
所以步势,從上面的分析中氧猬,我們能知道 對(duì)于一個(gè)相同的 name,我們獲取到的都是同一個(gè) SharedPreferencesImpl 對(duì)象坏瘩。
SharedPreferencesImpl
在上面的操作中盅抚,我們可以看到在第一次調(diào)用 getSharedPreferences
的時(shí)候,我們會(huì)去構(gòu)造一個(gè) SharedPreferencesImpl
對(duì)象倔矾,我們來(lái)看看都做了什么妄均。
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk();
}
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
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<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);
} 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();
}
}
}
注意看我們的 startLoadFromDisk
方法,我們會(huì)去新開(kāi)一個(gè)子線程哪自,然后去通過(guò) XmlUtils.readMapXml()
方法把指定的 SharedPreferences
文件的所有的鍵值對(duì)都讀出來(lái)丰包,然后存放到一個(gè) map 中。
而眾所周知壤巷,文件的讀寫(xiě)操作都是耗時(shí)的邑彪,可想而知,在我們第一次去讀取一個(gè) SharedPreferences
文件的時(shí)候花上了太多的時(shí)間會(huì)怎樣胧华。
SharedPreferences 的讀取操作
上面講了初次獲取一個(gè)文件的 SharedPreferences
實(shí)例的時(shí)候锌蓄,會(huì)先去把所有鍵值對(duì)讀取到緩存中升筏,這明顯是一個(gè)耗時(shí)操作,而我們正常的去讀取數(shù)據(jù)的時(shí)候瘸爽,都是類似這樣的代碼您访。
val sharedPreferences = getSharedPreferences("123", Context.MODE_PRIVATE)
val string = sharedPreferences.getString("123","")
SharedPreferences
的getXXX()
方法可能會(huì)報(bào)ClassCastException
異常,所以我們?cè)谕粋€(gè) name 的時(shí)候剪决,對(duì)不一樣的類型灵汪,必須使用不同的 key。但是putXXX
是可以用不同的類型值覆蓋相同的 key 的柑潦。
那勢(shì)必可能會(huì)導(dǎo)致這個(gè)操作需要等待一定的時(shí)間享言,我們姑且可以這么猜想,在 getXXX()
方法執(zhí)行的時(shí)候應(yīng)該是會(huì)等待前面的操作完成才能執(zhí)行的渗鬼。
因?yàn)?SharedPreferences
是一個(gè)接口览露,所以我們主要來(lái)看看它的實(shí)現(xiàn)類 SharedPreferencesImpl
,這里以 getString()
為例譬胎。
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
awaitLoadedLocked()
方法應(yīng)該就是我們所想的等待執(zhí)行操作了差牛,我們看看里面做了什么。
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);
}
}
可以看到堰乔,在 awaitLoadedLocked
方法里面我們使用了 mLock.wait()
來(lái)等待初始化的讀取操作偏化,而我們前面看到的 loadFromDiskLocked()
方法的最后也可以看到它調(diào)用了 mLock.notifyAll()
方法來(lái)喚醒后面這個(gè)阻塞的 getXXX()
。那么這里就會(huì)明顯出現(xiàn)一個(gè)問(wèn)題镐侯,我們的 getXXX()
方法是寫(xiě)在 UI 線程的侦讨,如果這個(gè)方法被阻塞的太久,勢(shì)必會(huì)出現(xiàn) ANR 的情況苟翻。所以我們一定在平時(shí)需要根據(jù)具體情況考慮是否需要把 SharedPreferences
的讀寫(xiě)操作放在子線程中韵卤。
SharedPreferences 的內(nèi)部類 Editor
我們?cè)趯?xiě)入數(shù)據(jù)之前,總是要先通過(guò)類似這樣的代碼獲取 SharedPreferences
的內(nèi)部類 Editor
崇猫。
val editor = sharedPreferences.edit()
我們當(dāng)然要看看這個(gè)到底是什么東西沈条。
@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) {
awaitLoadedLocked();
}
return new EditorImpl();
}
我們?cè)?/p>
可以看到,我們?cè)谧x取解析完 XML 文件的時(shí)候邓尤,直接返回了一個(gè) Editor
的實(shí)現(xiàn)類 EditorImpl
拍鲤。我們隨便查看一個(gè) putXXX 的方法一看贴谎。
private final Object mEditorLock = new Object();
@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;
}
}
可以看到汞扎,我們?cè)?EditorImpl
里面使用了一個(gè) HashMap
來(lái)存放我們的鍵值對(duì)數(shù)據(jù),每次 put 的時(shí)候都會(huì)直接往這個(gè)鍵值對(duì)變量 mModified
中進(jìn)行數(shù)據(jù)的 put 操作擅这。
commit() 和 apply()
我們總是在更新數(shù)據(jù)后需要加上 commit()
或者 apply()
來(lái)進(jìn)行輸入的寫(xiě)入操作澈魄,我們不妨來(lái)看看他們的實(shí)現(xiàn)到底有什么區(qū)別。
先看 commit() 和 apply() 的源碼仲翎。
@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;
}
@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);
// 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);
}
可以看到痹扇,apply()
和 commit()
的區(qū)別是在 commit()
把內(nèi)容同步提交到了硬盤(pán)铛漓,而 apply()
是先立即把修改提交給了內(nèi)存,然后開(kāi)啟了一個(gè)異步的線程提交到硬盤(pán)鲫构。commit()
會(huì)接收 MemoryCommitResult
里面的一個(gè) boolean
參數(shù)作為結(jié)果浓恶,而 apply()
沒(méi)有對(duì)結(jié)果做任何關(guān)心。
我們可以看到结笨,文件寫(xiě)入更新的操作都是交給 commitToMemory()
做的包晰,這個(gè)方法返回了一個(gè) MemoryCommitResult
對(duì)象,我們來(lái)看看到底做了什么炕吸。
// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
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;
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) {
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
即存放當(dāng)前 SharedPreferences
文件中的鍵值對(duì),而 mModified
則存放的是當(dāng)時(shí) edit()
時(shí) put 進(jìn)去的鍵值對(duì)赫模,這個(gè)我們前面有所介紹树肃。這里有個(gè) mDiskWritesInFlight
看起來(lái)應(yīng)該是表示正在等待寫(xiě)的操作數(shù)量。
接下來(lái)我們首先處理了 edit().clear()
操作的 mClear
標(biāo)志瀑罗,當(dāng)我們?cè)谕饷嬲{(diào)用 clear()
方法的時(shí)候胸嘴,我們會(huì)把 mClear
設(shè)置為 true,這時(shí)候我們會(huì)直接通過(guò) mMap.clear()
清空此時(shí)文件中的鍵值對(duì)廓脆,然后再遍歷 mModified
中新 put 進(jìn)來(lái)的鍵值對(duì)數(shù)據(jù)放到 mMap
中筛谚。也就是說(shuō):在一次提交中,如果我們又有 put 又有 clear()
操作的話停忿,我們只能 clear()
掉之前的鍵值對(duì)驾讲,這次 put()
進(jìn)去的鍵值對(duì)還是會(huì)被寫(xiě)入到 XML 文件中。
// 讀取
val sharedPreferences = getSharedPreferences("123", Context.MODE_PRIVATE)
// 寫(xiě)入
val editor = sharedPreferences.edit()
editor.putInt("1", 123)
editor.clear()
editor.apply()
Log.e("nanchen2251", "${sharedPreferences.getInt("1", 0)}")
也就是說(shuō)席赂,當(dāng)我們編寫(xiě)下面的代碼的時(shí)候吮铭,得到的打印還是 123。
然后我們接著往下看颅停,又發(fā)現(xiàn)了另外一個(gè) commit()
和 apply()
都做了調(diào)用的方法是 enqueueDiskWrite()
谓晌。
/**
* Enqueue an already-committed-to-memory result to be written
* to disk.
*
* They will be written to disk one-at-a-time in the order
* that they're enqueued.
*
* @param postWriteRunnable if non-null, we're being called
* from apply() and this is the runnable to run after
* the write proceeds. if null (from a regular commit()),
* then we're allowed to do this disk write on the main
* thread (which in addition to reducing allocations and
* creating a background thread, this has the advantage that
* we catch them in userdebug StrictMode reports to convert
* them where possible to apply() ...)
*/
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);
}
在這個(gè)方法中,首先通過(guò)判斷 postWriteRunnable
是否為 null 來(lái)判斷是 apply()
還是 commit()
癞揉。然后定義了一個(gè) Runnable
任務(wù)纸肉,在 Runnable
中先調(diào)用了 writeToFile()
進(jìn)行了寫(xiě)入和計(jì)數(shù)器更新的操作。
然后我們?cè)賮?lái)看看這個(gè) writeToFile()
方法做了些什么喊熟。
@GuardedBy("mWritingToDiskLock")
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
long startTime = 0;
long existsTime = 0;
long backupExistsTime = 0;
long outputStreamCreateTime = 0;
long writeTime = 0;
long fsyncTime = 0;
long setPermTime = 0;
long fstatTime = 0;
long deleteTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
boolean fileExists = mFile.exists();
if (DEBUG) {
existsTime = System.currentTimeMillis();
// Might not be set, hence init them to a default value
backupExistsTime = existsTime;
}
// Rename the current file so it may be used as a backup during the next read
if (fileExists) {
boolean needsWrite = false;
// Only need to write if the disk state is older than this commit
if (mDiskStateGeneration < mcr.memoryStateGeneration) {
if (isFromSyncCommit) {
needsWrite = true;
} else {
synchronized (mLock) {
// No need to persist intermediate states. Just wait for the latest state to
// be persisted.
if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
needsWrite = true;
}
}
}
}
if (!needsWrite) {
mcr.setDiskWriteResult(false, true);
return;
}
boolean backupFileExists = mBackupFile.exists();
if (DEBUG) {
backupExistsTime = System.currentTimeMillis();
}
// 此處需要注意一下
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);
if (DEBUG) {
outputStreamCreateTime = System.currentTimeMillis();
}
if (str == null) {
mcr.setDiskWriteResult(false, false);
return;
}
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
writeTime = System.currentTimeMillis();
FileUtils.sync(str);
fsyncTime = System.currentTimeMillis();
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
if (DEBUG) {
setPermTime = System.currentTimeMillis();
}
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();
if (DEBUG) {
deleteTime = System.currentTimeMillis();
}
mDiskStateGeneration = mcr.memoryStateGeneration;
mcr.setDiskWriteResult(true, true);
if (DEBUG) {
Log.d(TAG, "write: " + (existsTime - startTime) + "/"
+ (backupExistsTime - startTime) + "/"
+ (outputStreamCreateTime - startTime) + "/"
+ (writeTime - startTime) + "/"
+ (fsyncTime - startTime) + "/"
+ (setPermTime - startTime) + "/"
+ (fstatTime - startTime) + "/"
+ (deleteTime - startTime));
}
long fsyncDuration = fsyncTime - writeTime;
mSyncTimes.add((int) fsyncDuration);
mNumSync++;
if (DEBUG || mNumSync % 1024 == 0 || fsyncDuration > MAX_FSYNC_DURATION_MILLIS) {
mSyncTimes.log(TAG, "Time required to fsync " + mFile + ": ");
}
return;
} catch (XmlPullParserException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
} catch (IOException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
}
// Clean up an unsuccessfully written file
if (mFile.exists()) {
if (!mFile.delete()) {
Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
}
}
mcr.setDiskWriteResult(false, false);
}
代碼比較長(zhǎng)柏肪,做了一些時(shí)間的記錄和 XML 的相關(guān)處理,但最值得我們關(guān)注的還是其中打了標(biāo)注的對(duì)于 mBackupFile
的處理芥牌。我們可以明顯地看到烦味,在我們寫(xiě)入文件的時(shí)候,我們會(huì)把此前的 XML 文件改名為一個(gè)備份文件壁拉,然后再將要寫(xiě)入的數(shù)據(jù)寫(xiě)入到一個(gè)新的文件中谬俄。如果這個(gè)過(guò)程執(zhí)行成功的話柏靶,就會(huì)把備份文件刪除。由此可見(jiàn):即使我們每次只是添加一個(gè)鍵值對(duì)溃论,也會(huì)重新寫(xiě)入整個(gè)文件的數(shù)據(jù)屎蜓,這也說(shuō)明了 SharedPreferences 只適合保存少量數(shù)據(jù),文件太大會(huì)有性能問(wèn)題钥勋。
看完了這個(gè) writeToFile()
楞抡,我們?cè)賮?lái)看看下面做了啥述召。
// 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);
可以看到褥伴,當(dāng)且僅當(dāng)是 commit()
并且只有一個(gè)待寫(xiě)入操作的時(shí)候才能直接執(zhí)行到 writeToDiskRunnable.run()
萧恕,否則都會(huì)執(zhí)行到 QueuedWork
的 queue()
方法,這個(gè) QueuedWork
又是什么東西乎婿?
/** Finishers {@link #addFinisher added} and not yet {@link #removeFinisher removed} */
@GuardedBy("sLock")
private static final LinkedList<Runnable> sFinishers = new LinkedList<>();
/** Work queued via {@link #queue} */
@GuardedBy("sLock")
private static final LinkedList<Runnable> sWork = new LinkedList<>();
/**
* Internal utility class to keep track of process-global work that's outstanding and hasn't been
* finished yet.
*
* New work will be {@link #queue queued}.
*
* It is possible to add 'finisher'-runnables that are {@link #waitToFinish guaranteed to be run}.
* This is used to make sure the work has been finished.
*
* This was created for writing SharedPreference edits out asynchronously so we'd have a mechanism
* to wait for the writes in Activity.onPause and similar places, but we may use this mechanism for
* other things in the future.
*
* The queued asynchronous work is performed on a separate, dedicated thread.
*
* @hide
*/
public class QueuedWork {
/**
* Add a finisher-runnable to wait for {@link #queue asynchronously processed work}.
*
* Used by SharedPreferences$Editor#startCommit().
*
* Note that this doesn't actually start it running. This is just a scratch set for callers
* doing async work to keep updated with what's in-flight. In the common case, caller code
* (e.g. SharedPreferences) will pretty quickly call remove() after an add(). The only time
* these Runnables are run is from {@link #waitToFinish}.
*
* @param finisher The runnable to add as finisher
*/
public static void addFinisher(Runnable finisher) {
synchronized (sLock) {
sFinishers.add(finisher);
}
}
/**
* Remove a previously {@link #addFinisher added} finisher-runnable.
*
* @param finisher The runnable to remove.
*/
public static void removeFinisher(Runnable finisher) {
synchronized (sLock) {
sFinishers.remove(finisher);
}
}
/**
* Trigger queued work to be processed immediately. The queued work is processed on a separate
* thread asynchronous. While doing that run and process all finishers on this thread. The
* finishers can be implemented in a way to check weather the queued work is finished.
*
* Is called from the Activity base class's onPause(), after BroadcastReceiver's onReceive,
* after Service command handling, etc. (so async work is never lost)
*/
public static void waitToFinish() {
long startTime = System.currentTimeMillis();
boolean hadMessages = false;
Handler handler = getHandler();
synchronized (sLock) {
if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
// Delayed work will be processed at processPendingWork() below
handler.removeMessages(QueuedWorkHandler.MSG_RUN);
if (DEBUG) {
hadMessages = true;
Log.d(LOG_TAG, "waiting");
}
}
// We should not delay any work as this might delay the finishers
sCanDelay = false;
}
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
try {
processPendingWork();
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
try {
while (true) {
Runnable finisher;
synchronized (sLock) {
finisher = sFinishers.poll();
}
if (finisher == null) {
break;
}
finisher.run();
}
} finally {
sCanDelay = true;
}
synchronized (sLock) {
long waitTime = System.currentTimeMillis() - startTime;
if (waitTime > 0 || hadMessages) {
mWaitTimes.add(Long.valueOf(waitTime).intValue());
mNumWaits++;
if (DEBUG || mNumWaits % 1024 == 0 || waitTime > MAX_WAIT_TIME_MILLIS) {
mWaitTimes.log(LOG_TAG, "waited: ");
}
}
}
}
/**
* Queue a work-runnable for processing asynchronously.
*
* @param work The new runnable to process
* @param shouldDelay If the message should be delayed
*/
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);
}
}
}
}
簡(jiǎn)單地說(shuō)测僵,這個(gè) QueuedWork
類里面有一個(gè)專門(mén)存放 Runnable
的兩個(gè) LinkedList
對(duì)象,他們分別對(duì)應(yīng)未完成的操作 sFinishers
和正在工作的 sWork
谢翎。
我們?cè)?waitToFinish()
方法中捍靠,會(huì)不斷地去遍歷執(zhí)行未完成的 Runnable
。我們根據(jù)注釋也知道了這個(gè)方法會(huì)在 Activity
的 onPause()
和 BroadcastReceiver
的 onReceive()
方法后調(diào)用森逮。假設(shè)我們頻繁的調(diào)用了 apply()
方法榨婆,并緊接著調(diào)用了 onPause()
,那么就可能會(huì)發(fā)生 onPause()
一直等待 QueuedWork.waitToFinish
執(zhí)行完成而產(chǎn)生 ANR褒侧。也就是說(shuō)良风,即使是調(diào)用了 apply()
方法去異步提交,也不是完全安全的闷供。如果 apply()
方法使用不當(dāng)烟央,也是可能出現(xiàn) ANR 的。
總結(jié)
說(shuō)了這么多歪脏,我們當(dāng)然還是需要做一個(gè)總結(jié)疑俭。
-
apply()
沒(méi)有返回值而commit()
返回boolean
表明修改是否提交成功 ; -
commit()
是把內(nèi)容同步提交到硬盤(pán)的婿失,而apply()
先立即把修改提交到內(nèi)存钞艇,然后開(kāi)啟一個(gè)異步的線程提交到硬盤(pán),并且如果提交失敗豪硅,你不會(huì)收到任何通知哩照。 - 所有
commit()
提交是同步過(guò)程,效率會(huì)比apply()
異步提交的速度慢舟误,在不關(guān)心提交結(jié)果是否成功的情況下葡秒,優(yōu)先考慮apply()
方法姻乓。 -
apply()
是使用異步線程寫(xiě)入磁盤(pán)嵌溢,commit()
是同步寫(xiě)入磁盤(pán)眯牧。所以我們?cè)谥骶€程使用的commit()
的時(shí)候,需要考慮是否會(huì)出現(xiàn) ANR 問(wèn)題赖草。 - 我們每次添加鍵值對(duì)的時(shí)候学少,都會(huì)重新寫(xiě)入整個(gè)文件的數(shù)據(jù),所以它不適合大量數(shù)據(jù)存儲(chǔ)秧骑。
- 多線程場(chǎng)景下效率比較低版确,因?yàn)?get 操作的時(shí)候,會(huì)鎖定
SharedPreferencesImpl
里面的對(duì)象乎折,互斥其他操作绒疗,而當(dāng)put
、commit()
和apply()
操作的時(shí)候都會(huì)鎖住Editor
的對(duì)象骂澄,在這樣的情況下吓蘑,效率會(huì)降低。 - 由于每次都會(huì)把整個(gè)文件加載到內(nèi)存中坟冲,因此磨镶,如果 SharedPreferences 文件過(guò)大,或者在其中的鍵值對(duì)是大對(duì)象的 JSON 數(shù)據(jù)則會(huì)占用大量?jī)?nèi)存健提,讀取較慢是一方面琳猫,同時(shí)也會(huì)引發(fā)程序頻繁 GC,導(dǎo)致的界面卡頓私痹。
- get 操作都是線程安全的脐嫂, 并且 get 僅僅是從內(nèi)存中 (mMap) 獲取數(shù)據(jù), 所以無(wú)性能問(wèn)題。
基于以上缺點(diǎn): - 建議不要存儲(chǔ)較大數(shù)據(jù)到
SharedPreferences
紊遵,也不要把較多數(shù)據(jù)存儲(chǔ)到同一個(gè) name 對(duì)應(yīng)的SharedPreferences
中雹锣,最好根據(jù)規(guī)則拆分為多個(gè)SharedPreferences
文件。 - 頻繁修改的數(shù)據(jù)修改后統(tǒng)一提交癞蚕,而不是修改過(guò)后馬上提交蕊爵。
- 在跨進(jìn)程通訊中不去使用
SharedPreferences
。 - 獲取
SharedPreferences
對(duì)象的時(shí)候會(huì)讀取SharedPreferences
文件桦山,如果文件沒(méi)有讀取完攒射,就執(zhí)行了 get 和 put 操作,可能會(huì)出現(xiàn)需要等待的情況恒水,因此最好提前獲取SharedPreferences
對(duì)象会放。 - 每次調(diào)用
edit()
方法都會(huì)創(chuàng)建一個(gè)新的EditorImpl
對(duì)象,不要頻繁調(diào)用edit()
方法钉凌。
參考鏈接:https://juejin.im/post/5adc444df265da0b886d00bc#heading-10