提起SharedPreferences,應(yīng)該所有的開發(fā)者都覺得很簡單吧,會不會想:這個有啥好說的,還寫博客?是不是裝X啊?
其實(shí)我也覺得SharedPreferences很簡單,除非你要用它實(shí)現(xiàn)多進(jìn)程之間的數(shù)據(jù)共享。說到多進(jìn)程之間使用SharedPreferences,會不會有人想說:創(chuàng)建它的時候不是有個參數(shù)Context.MODE_MULTI_PROCESS,這不是android官方提供的多進(jìn)程支持嗎?呵呵,少年,too young too simple??!
Note: currently this class does not support use across multiple processes. This will be added later.
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 asContentProvider.
以上摘自android官方文檔SharedPreferences和Context介紹部分
拋開多進(jìn)程之間使用,在自己的App中使用SharedPreferences還是很簡單的。當(dāng)然這篇文章我不是說開發(fā)中如何使用它,而是記錄自己在優(yōu)化我們的App啟動過程中發(fā)現(xiàn)的一些問題--SharedPreferences導(dǎo)致的問題。
進(jìn)入正題
引子
SharedPreferences是Android SDK提供的工具,可以存儲應(yīng)用的一些配置信息,這些信息會以鍵值對的形式保存在/sdcard/data/data/packageName/shared_prefs/路徑下的一個xml文件中。它提供了多種數(shù)據(jù)類型的存儲,包括:int、long、boolean、float、String以及Set<String>。
我真正想記錄的
獲取SharedPreferences的實(shí)例一般會有兩種方式:
context.getSharedPreference(name, mode);-
PreferenceManager.getDefaultSharedPreferences(context);
這兩種方式其實(shí)具體實(shí)現(xiàn)是一樣的,只不過一個是開發(fā)者自己定義名字,另一個是使用包名+"_preference"作為存儲文件名。
getSharedPreference(name, mode)
public SharedPreferences getSharedPreferences(String name, int mode) {
return mBase.getSharedPreferences(name, mode);
}
PreferenceManager.getDefaultSharedPreferences(context)
public static SharedPreferences getDefaultSharedPreferences(Context context) {
return
context.getSharedPreferences(getDefaultSharedPreferencesName(context),
getDefaultSharedPreferencesMode());
}
private static String getDefaultSharedPreferencesName(Context context) {
return context.getPackageName() + "_preferences";
}
private static int getDefaultSharedPreferencesMode() {
return Context.MODE_PRIVATE;
}
我們的App中充斥著大量的PreferenceManager.getDefaultSharedPreferences(context)使用方式,可能是大家覺得方便,又是API級的Manager管理,所以使用起來很放心,然而這也正是問題的根本所在。問題在哪里呢?接下來便告訴你。
因?yàn)樵谧鯝pp的優(yōu)化,自然少不了使用MethodTrace,在分析啟動流程時發(fā)現(xiàn)一個很詭異的點(diǎn),那就是一個讀取配置操作竟然耗時400+ms的時間(有圖為證),

我當(dāng)時就被震驚了,這里竟然耗時這么久!作為一個碼農(nóng),這時必須要看源碼呀,可惜通過AS并不能直接看到實(shí)現(xiàn)邏輯,不過通過Google還是可以輕松找到的,有興趣的看這里。
SharedPreferences是一個抽象類,所以具體實(shí)現(xiàn)的邏輯便在其子類SharedPreferencesImpl中,先看下它的構(gòu)造方法:
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();//可以看到,當(dāng)你使用時,這里變開始從sdcard讀取xml文件
}
startLoadFromDisk()具體實(shí)現(xiàn)代碼如下:
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
synchronized (SharedPreferencesImpl.this) {
loadFromDiskLocked();
}
}
}.start();
}
private void loadFromDiskLocked() {
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 = 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);
} catch (XmlPullParserException e) {
Log.w(TAG, "getSharedPreferences", e);
} catch (FileNotFoundException e) {
Log.w(TAG, "getSharedPreferences", e);
} catch (IOException e) {
Log.w(TAG, "getSharedPreferences", e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
}
mLoaded = true;
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<String, Object>();
}
notifyAll();
}
再看看get方法:
public String getString(String key, String defValue) {
synchronized (this) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
從代碼可以看到,在get之前是要調(diào)用awaitLoadedLocked ()方法的,那這個方法具體做了什么呢?看代碼:
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 {
wait();
} catch (InterruptedException unused) {
}
}
}
也許你已經(jīng)看出來了,在真正get數(shù)據(jù)之前是要判斷文件加載狀態(tài)的。如果此時沒加載出來,那就要等待。這時你有沒有理解為什么第一次調(diào)用要花費(fèi)400+ms的時間(就是MethodTrace圖上顯示的時間,雖然MethodTrace方法本身會增加方法的調(diào)用時間,但基本上還是可以反應(yīng)出實(shí)際調(diào)用時間長短的)了嗎?是不是心里在想:你的App中使用大量PreferenceManger獲取SharedPreferences對象來存儲配置信息,導(dǎo)致該文件變得很大,文件大肯定加載時間要長嘛。Bingo,you get it。前面鋪墊那么多,就是為了這一點(diǎn),文件大自然要等待很久,所以如果你的App對初始化時間敏感,并且你配置文件中存了太多的東西,那么你應(yīng)該考慮把初始化所需要的配置放在單獨(dú)的文件中(自然是使用context.getSharedPreferences()方法,傳入單獨(dú)的名稱),而不是所有的配置放一起了。
后記:SharedPreferencesImpl的實(shí)例是有緩存的,cache在ContextImpl類中,所以在load之后,下次再調(diào)用SharedPreferences.getXXX()方法時你將看不到漫長的wait時間了。