Android SharedPreference 原始碼分析(一)
1. 前言
眾所周知,SharedPreferences是Android平臺上一個輕量級的儲存類,用來儲存應用的一些常用配置,比如Activity狀態,Activity暫停時,將此activity的狀態儲存到SharedPereferences中;當Activity過載,系統回撥方法onSaveInstanceState時,再從SharedPreferences中將值取出。
2.基本概念
SharedPreferences提供了常規的Long、Int、String等型別資料的儲存/獲取介面,並以xml方式儲存在data/data/you.package.name/shared_prefs/目錄下的檔案。
SharedPreferences資料的操作模式
Context.MODE_PRIVATE:為預設操作模式,代表該檔案是私有資料,只能被應用本身訪問,在該模式下,寫入的內容會覆蓋原檔案的內容
Context.MODE_APPEND:模式會檢查檔案是否存在,存在就往檔案追加內容,否則就建立新檔案.
Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE用來控制其他應用是否有許可權讀寫該檔案.
MODE_WORLD_READABLE:表示當前檔案可以被其他應用讀取.
MODE_WORLD_WRITEABLE:表示當前檔案可以被其他應用寫入
特別注意:出於安全性的考慮,MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE 在Android 4.2版本中已經被棄用。
MODE_MULTI_PROCESS:跨程序模式
3.提出問題
1).SharedPreferences真的支援多程序嗎?
可能有些人會覺得很奇怪,上面不是寫了MODE_MULTI_PROCESS模式,明確說明就是跨程序模式,為什麼還問是否真的支援多程序。為了找到答案,我們來閱讀MODE_MULTI_PROCESS的註釋。
/**
* SharedPreference loading flag: when set, the file on disk will
* be checked for modification even if the shared preferences
* instance is already loaded in this process. This behavior is
* sometimes desired in cases where the application has multiple
* processes, all writing to the same SharedPreferences file.
* Generally there are better forms of communication between
* processes, though.
*
* <p>This was the legacy (but undocumented) behavior in and
* before Gingerbread (Android 2.3) and this flag is implied when
* targetting such releases. For applications targetting SDK
* versions <em>greater than</em> Android 2.3, this flag must be
* explicitly set if desired.
*
* @see #getSharedPreferences
*
* @deprecated 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 as {@link android.content.ContentProvider ContentProvider}.
*/
很尷尬,英文看不懂?google翻譯一下。
/ **
* SharedPreference載入標誌:設定時,磁碟上的檔案將
*即使共享首選項,也要檢查修改
*例項已經在這個過程中載入。這是行為
*有時需要在應用程式有多個的情況下
*程序,全部寫入相同的SharedPreferences檔案。
*一般來說有更好的交流形式
*程序,雖然。
*
*這是在和遺傳(但沒有記錄)的行為
*之前的薑餅(Android 2.3)和這個標誌是隱含的時候
*瞄準這樣的釋出。針對SDK的應用程式
*版本<em>大於</ em> Android 2.3,這個標誌必須是
*如果需要顯式設定。
*
*參見#getSharedPreferences
*
* @deprecated MODE_MULTI_PROCESS不能可靠的工作
*某些版本的Android,而且不提供任何
*協調跨越併發修改的機制
*程序。應用程式不應該嘗試使用它。代替,
*他們應該使用明確的跨程序資料管理
*方法,如{@link android.content.ContentProvider ContentProvider}。
* /
很明顯MODE_MULTI_PROCESS是不可靠的,google推薦使用ContentProvider實現跨程序資料管理,如何使用,這個在下一篇介紹。
2).上面說到SharedPreference是以xml檔案存在本地,那麼讀取/儲存檔案是不是損耗效能,需不需要每個app自己做記憶體快取,還有儲存資料大小是否有限制,頻繁讀取有沒有注意點,帶著這些問題我們看看SharedPreference底層是如何實現的。
4.原始碼分析
1)先上圖,讓大家直觀的瞭解下SharedPreference相關的類以及相關關係。
2)一般使用SharedPreference的時候,都是如下呼叫:
SharedPreferences sp = context.getSharedPreferences(SP_NAME,
Context.MODE_MULTI_PROCESS);
那麼,我們就看下getSharedPreferences(String name, int mode)方法,開啟Context類可以看到該方法是個抽象方法,具體實現在子類中,開啟Context的實現類ContextImpl,可以看到getSharedPreferences(String name, int mode)原始碼如下所示:
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
if (sSharedPrefs == null) {
sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
}
final String packageName = getPackageName();
ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();
sSharedPrefs.put(packageName, packagePrefs);
}
// 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";
}
}
sp = packagePrefs.get(name);
if (sp == null) {
File prefsFile = getSharedPrefsFile(name);
sp = new SharedPreferencesImpl(prefsFile, mode);
packagePrefs.put(name, 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;
}
從5-14行可以看出,先根據應用包名,從本地map中取到該應用的所有sp檔案,在根據傳進來的name,獲取需要讀取的sp檔案。
從27-32行可以看出,第一次先拿到sp檔案,然後初始化出SharedPreferencesImpl實現類,在SharedPreferencesImpl的構造方法中從本地讀取了該name的sp檔案,後面會分析。
從34-41行看出,如果是MODE_MULTI_PROCESS模式或者版本小於11,會呼叫sp.startReloadIfChangedUnexpectedly()方法,該方法從磁碟把檔案重新讀取到記憶體,原始碼如下:
void startReloadIfChangedUnexpectedly() {
synchronized (this) {
// TODO: wait for any pending writes to disk?
if (!hasFileChangedUnexpectedly()) {
return;
}
startLoadFromDisk();
}
}
剛剛說到SharedPreferencesImpl的構造方法中從本地讀取了該name的sp檔案,原始碼如下所示:
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();
}
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();
}
從上面程式碼可以看出,從sp檔案讀出資料後,會賦值給mMap物件,該物件通過鍵值對儲存sp檔案,實現二級快取的目的是為了提高讀取效率,所以app應用中就不需要單獨再自己做快取。既然資料都儲存在記憶體快取mMap中,這樣也就說明sp檔案不適合儲存大資料,會十分浪費記憶體。
下面再看下,如何儲存資料,我們一般使用兩種方法:apply(),commit()
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);
}
第二行可以看出,先儲存到記憶體,通過第十二行,可以看出,通過非同步執行緒儲存sp檔案到磁碟。
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;
}
通過commit的原始碼可以看出,該儲存到記憶體跟磁碟是同步儲存,所以,如果頻繁儲存資料的話,apply肯定要高效,優先推薦使用apply。
看完儲存資料,下面我們來看如何讀取資料,拿getString來做示例:
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (this) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
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) {
}
}
}
可以看到資料是從mMap中讀取,也就是從記憶體快取中取,這樣省去io操作,提高效能。
裡面還有一個boolean變數mLoaded,該變數預設為false,這樣會一直阻塞住讀取,在什麼地方設定為true的呢,從mLoaded的字面可以猜到是載入,那應該是從本地磁碟讀成功賦值為true,我們看下原始碼:
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();
}
很明顯,最後讀取成功後,mLoaded 被賦值為true,而loadFromDiskLocked這個方法是在SharedPreferencesImpl的構造方法中就呼叫,所以為了不造成阻塞,我們可以提前創建出SharedPreferences物件,而不是在使用的時候再去建立。
總結:
1.sp資料都儲存在記憶體快取mMap中,這樣也就說明sp檔案不適合儲存大資料,會十分浪費記憶體。
2.apply()先儲存到記憶體,再通過非同步執行緒儲存sp檔案到磁碟,commit儲存到記憶體跟磁碟是同步儲存,所以,如果頻繁儲存資料的話,apply肯定要高效,優先推薦使用apply。
3.從sp中讀取資料,需要等sp構造方法呼叫從磁碟讀取資料成功後才繼續往下執行,所以為了不造成阻塞,我們可以提前創建出SharedPreferences物件,而不是在使用的時候再去建立。
如有錯誤歡迎指出來,一起學習。
交流討論群
群號:469890293