本文目錄:
- 寫在前面
- 獲取 SharedPreferences 實(shí)例
- 加載 xml 數(shù)據(jù)文件
- 初次讀取數(shù)據(jù)的耗時(shí)分析
- commit 和 apply 的對(duì)比
寫在前面
SharedPreferences 平時(shí)用來(lái)持久化一些基本數(shù)據(jù)類型或者一些可序列化的對(duì)象。
根據(jù)我們?nèi)粘5慕?jīng)常,持久化操作是耗時(shí)的烙样,涉及到文件的 IO 操作,但是實(shí)際使用 SharedPreferences 時(shí)娶聘,發(fā)現(xiàn)只有第一次讀取數(shù)據(jù)是有概率卡主線程幾十到幾百毫秒,而之后的讀取時(shí)間幾乎可以忽略不計(jì)。
我們有了這樣的疑問:
- 為什么初次讀取數(shù)據(jù)會(huì)有概率的阻塞?
- 為什么除了初次讀取數(shù)據(jù)可能阻塞椎工,而可以在后面的讀取很快?
- 為什么都推薦使用 apply 而不是 commit 提交數(shù)據(jù)蜀踏?
帶著問題去理解它的實(shí)現(xiàn)维蒙。
獲取 SharedPreferences 實(shí)例
SharedPreferences 是由 Context 返回的,比如我們的 Application果覆,Activity颅痊。所以具體的實(shí)現(xiàn)每個(gè)應(yīng)用的上下文環(huán)境有關(guān),每個(gè)應(yīng)用有自己的單獨(dú)的文件夾存放這些數(shù)據(jù)局待,對(duì)其他應(yīng)用不可見斑响。
獲取 SharedPreferences 的方法定義在抽象類 Context 中:
public abstract SharedPreferences getSharedPreferences(String name, int mode);
public abstract SharedPreferences getSharedPreferences(File file, int mode);
如果查看 Application 或者 Activity 的源碼,會(huì)找不到具體的實(shí)現(xiàn)钳榨。這是因?yàn)樗鼈兝^承了 ContextWrapper舰罚,代理模式,代理 ContextImpl 的實(shí)例 mBase 中重绷。ContextImpl 是具體的實(shí)現(xiàn)沸停。
兩種獲取 SharedPreferences 的方法中,我們基本上用的是 getSharedPreferences(String name, int mode);
昭卓,參數(shù)只傳了文件的名字愤钾。
查看內(nèi)部的代碼可以看到,雖然只有一個(gè)名字候醒,ContextImpl 會(huì)構(gòu)建出文件的具體路徑能颁。再接著調(diào)用 getSharedPreferences(File file, int mode);
方法返回 SharedPreferencesImpl 實(shí)例。
所以 SharedPreferences 的操作倒淫,本質(zhì)上就是對(duì)文件的操作伙菊。最后會(huì)落實(shí)到一個(gè) xml 文件上:
@Override
public File getSharedPreferencesPath(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
標(biāo)準(zhǔn)路徑在 /data/data/應(yīng)用包名/shared_prefs
文件夾中,且都是 xml 文件。
創(chuàng)建好 File 對(duì)象后镜硕,會(huì)在 getSharedPreferences(File file, int mode)
中打開文件并執(zhí)行初始操作运翼,把 SharedPreferencesImpl 實(shí)例返回:
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
checkMode(mode);
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
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;
}
這里我們看到了另一個(gè)重要的類 SharedPreferencesImpl,它和 ContextImpl 一樣兴枯,是接口的具體實(shí)現(xiàn)類血淌。
每一個(gè) File 文件對(duì)應(yīng)一個(gè) SharedPreferencesImpl 實(shí)例。為了提高效率财剖,ContextImpl 有做緩存 cache悠夯,這里的緩存是強(qiáng)引用,在整個(gè)進(jìn)程的生命周期中都存在躺坟,意味著每個(gè)文件的 SharedPreferencesImpl 實(shí)例在整個(gè)進(jìn)程中只會(huì)被創(chuàng)建一次沦补。
這個(gè)方法的末尾有一個(gè)特殊的處理需要注意一下,是關(guān)于模式 Context.MODE_MULTI_PROCESS
咪橙,可以看到在這個(gè)模式下夕膀,會(huì)調(diào)用:
sp.startReloadIfChangedUnexpectedly();
這個(gè)方法執(zhí)行下去,會(huì)檢查文件是否被修改了匣摘,如果文件被修改了店诗,會(huì)調(diào)用 startLoadFromDisk
來(lái)更新文件。因?yàn)槎噙M(jìn)程環(huán)境下音榜,這里的文件有可能被其他進(jìn)程修改庞瘸。
加載 xml 數(shù)據(jù)文件
為什么除了初次讀取數(shù)據(jù)可能卡頓,而可以在后面的讀取很快赠叼?
我們進(jìn)入 SharedPreferences 的加載流程擦囊,就是把文件的內(nèi)容載入內(nèi)存的過程。
載入文件的方法在 startLoadFromDisk
中嘴办,顧名思義贯被,就是開始從磁盤加載數(shù)據(jù)彤灶。
調(diào)用該方法有兩個(gè)地方:
- 構(gòu)造函數(shù)里會(huì)被調(diào)用汽煮。所以第一次創(chuàng)建 SharedPreferencesImpl 會(huì)馬上把文件內(nèi)容載入內(nèi)存心例。
- 在
Context.MODE_MULTI_PROCESS
下摆寄,文件發(fā)生修改時(shí)被調(diào)用逗扒。目的就是多進(jìn)程下更新數(shù)據(jù)现恼。
startLoadFromDisk
方法如下:
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
可以看到直接開啟一個(gè)新線程,調(diào)用 loadFromDisk 加載文件:
private void loadFromDisk() {
...
str = new BufferedInputStream(
new FileInputStream(mFile), 16*1024);
map = XmlUtils.readMapXml(str);
...
}
本質(zhì)上棵里,就是讀取一個(gè) xml 文件,被內(nèi)容解析為 Map 對(duì)象。這個(gè) map 包含了我們之前保存的所有鍵值對(duì)的數(shù)據(jù)。并且把 map對(duì)象保存為 mMap 成員變量,直接在內(nèi)存中常駐:
synchronized (SharedPreferencesImpl.this) {
mLoaded = true;
if (map != null) {
mMap = map
...
} else {
mMap = new HashMap<>();
}
notifyAll();
}
這里可以解釋我們的疑問,為什么 SharedPreferences 的讀取非常快,載入完成后,后面的讀操作都是針對(duì) mMap 的郁岩,響應(yīng)速度是內(nèi)存級(jí)別的非称继快片酝。比如 getString:
public String getString(String key, @Nullable String defValue) {
synchronized (this) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
我們也就可以理解為什么 SharedPreferences 不希望存大量數(shù)據(jù)了审轮,一個(gè)很重要的原因也是內(nèi)存緩存崖飘,如果數(shù)據(jù)量很大的話达椰,這里會(huì)占據(jù)很大一塊內(nèi)存檀何。
初次讀取數(shù)據(jù)的耗時(shí)分析
為什么初次讀取數(shù)據(jù)會(huì)有概率的阻塞?
對(duì)應(yīng)用性能監(jiān)控中發(fā)現(xiàn),SharedPreferences 初次讀取數(shù)據(jù)的會(huì)發(fā)現(xiàn)概率發(fā)生阻塞羡藐,一般會(huì)被卡 20~40 ms瘩扼。如果系統(tǒng) IO 原本就繁忙的話谆棺,甚至可能會(huì)卡好幾秒蔼啦。
所以在應(yīng)用啟動(dòng)中奈籽,我們?nèi)カ@取一些配置,不得不在主線程對(duì) SharedPreferences 進(jìn)行初次操作猛计。如果在短時(shí)間內(nèi)讀取多個(gè) 不同的 SharedPreferences唠摹,應(yīng)用的啟動(dòng)會(huì)耗費(fèi)很長(zhǎng)的時(shí)間。
這和一個(gè)鎖有關(guān)奉瘤,就是 SharePreferencesImpl.this勾拉。在初始化加載文件的時(shí)候,和讀取數(shù)據(jù)的時(shí)候都會(huì)用到這個(gè)鎖盗温。
在 SharePreferencesImpl 構(gòu)造中藕赞,調(diào)用 loadFromDisk ,加鎖保護(hù)了對(duì) mLoad 和 mMap 的讀寫:
private void loadFromDisk() {
synchronized (SharedPreferencesImpl.this) {
...
}
}
而每次讀取數(shù)據(jù)的時(shí)候卖局,也加了這個(gè)鎖去保護(hù)這些成員斧蜕,比如 getString:
public String getString(String key, @Nullable String defValue) {
synchronized (this) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
所以這里形成了一個(gè)競(jìng)爭(zhēng)關(guān)系,如果在本地 xml 文件的加載過程中砚偶,先執(zhí)行了 loadFromDisk批销,那么 getString 就會(huì)阻塞等待。
loadFromDisk 是 IO 耗時(shí)操作染坯,雖然 loadFromDisk 操作被分配到另一個(gè)線程執(zhí)行均芽,但因?yàn)樽x取數(shù)據(jù)的時(shí)候,爭(zhēng)用了這個(gè)鎖单鹿,會(huì)發(fā)生概率卡頓掀宋。
commit 和 apply 的對(duì)比
為什么都推薦使用 apply 而不是 commit 提交數(shù)據(jù)?
先看我們平時(shí)修改 SharedPreferences 的姿勢(shì):
SharedPreferences sp = context.getSharedPreferences("test", Mode.PRIVATE);
Editor editor = sp.edit();
editor.putString("key", "Hello World!");
editor.commit(); 或者 editor.apply();
可以看到具體修改被它的內(nèi)部類 EditorImpl 接管仲锄,最后才調(diào)用 commit 或者 apply劲妙,而這兩者的區(qū)別就是我們要討論的。
EditorImpl 內(nèi)部有一個(gè)內(nèi)存緩存儒喊,用來(lái)保存用戶修改后的操作:
private final Map<String, Object> mModified = Maps.newHashMap();
在執(zhí)行 commit 或者 apply 前镣奋,比如上面的 editor.putString("key","Hello World!")
會(huì)把修改存儲(chǔ)在 mModified 中:
public Editor putString(String key, @Nullable String value) {
synchronized (this) {
mModified.put(key, value);
return this;
}
}
}
到這里,只是把修改緩存在了內(nèi)存中澄惊。然后調(diào)用 commit 和 apply 把修改持久化唆途。
這兩個(gè)方法都會(huì)調(diào)用一個(gè) commitToMemory 方法,做兩件事情:
- 一個(gè)是把修改提交到內(nèi)存
- 創(chuàng)建 MemoryCommitResult 用來(lái)做后面的本地 IO掸驱。
修改很簡(jiǎn)單肛搬,就是遍歷 mModified,把修改的內(nèi)容全部同步給 mMap毕贼。
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);
}
}
而 MemoryCommitResult 是一個(gè)數(shù)據(jù)容器温赔,記錄著一些后面進(jìn)行磁盤寫入操作需要使用到的數(shù)據(jù),比如有:
-
boolean changesMade
鬼癣,標(biāo)記變量陶贼,用來(lái)標(biāo)記數(shù)據(jù)是否發(fā)生改變啤贩。 -
Map<?, ?> mapToWriteToDisk
, 最終要寫入到本地的數(shù)據(jù)拜秧,會(huì)指向 SharedPreferencesImpl 的內(nèi)存緩存 mMap
同步修改到 mMap
經(jīng)過這個(gè)階段痹屹,內(nèi)存的數(shù)據(jù)就被更新了。并創(chuàng)建好 MemoryCommitResult 對(duì)象后枉氮,接下來(lái)就是不一樣的操作志衍。
先看 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;
}
在調(diào)用 enqueueDiskWrite 的時(shí)候,因?yàn)闆]有構(gòu)建 postWriteRunnable聊替,最終會(huì)在當(dāng)前線程直接執(zhí)行寫入操作:
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
...
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (SharedPreferencesImpl.this) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
...
}
直接調(diào)用 writeToDiskRunnable.run()
沒有再開線程楼肪,直接阻塞寫入。
apply 方法:
public void apply() {
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
QueuedWork.add(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.remove(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);
}
可以看到先構(gòu)造了一個(gè) postWriteRunnable 傳入 enqueueDiskWrite惹悄。
在方法的執(zhí)行中春叫,可以看到最后會(huì)在一個(gè)單線程線程池 QueuedWork.singleThreadExecutor()
中執(zhí)行寫入操作:
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
...
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
所以,commit 是阻塞的泣港,apply 是非阻塞的暂殖。
平時(shí)使用的時(shí)候,盡量使用 apply 避免卡主主線程当纱。因?yàn)閷懭肭岸家呀?jīng)更新修改到緩存了央星,不用擔(dān)心讀到臟數(shù)據(jù)。