SharedPreferences是Android提供的數(shù)據(jù)持久化的一種手段固以,適合單進(jìn)程、小批量的數(shù)據(jù)存儲與訪問嘱巾。為什么這么說呢憨琳?因?yàn)镾haredPreferences的實(shí)現(xiàn)是基于單個(gè)xml文件實(shí)現(xiàn)的,并且旬昭,所有持久化數(shù)據(jù)都是一次性加載到內(nèi)存篙螟,如果數(shù)據(jù)過大,是不合適采用SharedPreferences存放的问拘。而適用的場景是單進(jìn)程的原因同樣如此遍略,由于Android原生的文件訪問并不支持多進(jìn)程互斥,所以SharePreferences也不支持骤坐,如果多個(gè)進(jìn)程更新同一個(gè)xml文件绪杏,就可能存在同不互斥問題,后面會(huì)詳細(xì)分析這幾個(gè)問題纽绍。
SharedPreferences的實(shí)現(xiàn)原理之:持久化數(shù)據(jù)的加載
首先蕾久,從基本使用簡單看下SharedPreferences的實(shí)現(xiàn)原理:
mSharedPreferences = context.getSharedPreferences("test", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putString(key, value);
editor.apply();
context.getSharedPreferences其實(shí)就是簡單的調(diào)用ContextImpl的getSharedPreferences,具體實(shí)現(xiàn)如下
@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);
}
sp = packagePrefs.get(name);
if (sp == null) {
<!--讀取文件-->
File prefsFile = getSharedPrefsFile(name);
sp = new SharedPreferencesImpl(prefsFile, mode);
<!--緩存sp對象-->
packagePrefs.put(name, sp);
return sp;
}
}
<!--跨進(jìn)程同步問題-->
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
以上代碼非常簡單拌夏,直接描述下來就是先去內(nèi)存中查詢與xml對應(yīng)的SharePreferences是否已經(jīng)被創(chuàng)建加載僧著,如果沒有那么該創(chuàng)建就創(chuàng)建履因,該加載就加載,在加載之后盹愚,要將所有的key-value保存到內(nèi)幕才能中去栅迄,當(dāng)然,如果首次訪問皆怕,可能連xml文件都不存在毅舆,那么還需要?jiǎng)?chuàng)建xml文件,與SharePreferences對應(yīng)的xml文件位置一般都在/data/data/包名/shared_prefs目錄下愈腾,后綴一定是.xml朗兵,數(shù)據(jù)存儲樣式如下
這里面數(shù)據(jù)的加載的地方需要看下,比如顶滩,SharePreferences數(shù)據(jù)的加載是同步還是異步?數(shù)據(jù)加載是new SharedPreferencesImpl對象時(shí)候開始的寸爆,
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();
}
startLoadFromDisk很簡單礁鲁,就是讀取xml配置,如果其他線程想要在讀取之前就是用的話赁豆,就會(huì)被阻塞仅醇,一直wait等待,直到數(shù)據(jù)讀取完成魔种。
private void loadFromDiskLocked() {
...
Map map = null;
StructStat stat = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
<!--讀取xml中配置-->
str = new BufferedInputStream(
new FileInputStream(mFile), 16*1024);
map = XmlUtils.readMapXml(str);
}...
mLoaded = true;
...
<!--喚起其他等待線程-->
notifyAll();
}
可以看到其實(shí)就是直接使用xml解析工具XmlUtils析二,直接在當(dāng)前線程讀取xml文件,所以节预,如果xml文件稍大叶摄,盡量不要在主線程讀取,讀取完成之后安拟,xml中的配置項(xiàng)都會(huì)被加載到內(nèi)存蛤吓,再次訪問的時(shí)候,其實(shí)訪問的是內(nèi)存緩存糠赦。
SharedPreferences的實(shí)現(xiàn)原理之:持久化數(shù)據(jù)的更新
通常更新SharedPreferences的時(shí)候是首先獲取一個(gè)SharedPreferences.Editor会傲,利用它緩存一批操作,之后當(dāng)做事務(wù)提交拙泽,有點(diǎn)類似于數(shù)據(jù)庫的批量更新:
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putString(key1, value1);
editor.putString(key2, value2);
editor.putString(key3, value3);
editor.apply();//或者commit
Editor是一個(gè)接口淌山,這里的實(shí)現(xiàn)是一個(gè)EditorImpl對象,它首先批量預(yù)處理更新操作顾瞻,之后再提交更新泼疑,在提交事務(wù)的時(shí)候有兩種方式,一種是apply朋其,另一種commit王浴,兩者的區(qū)別在于:何時(shí)將數(shù)據(jù)持久化到xml文件脆炎,前者是異步的,后者是同步的氓辣。Google推薦使用前一種秒裕,因?yàn)椋蛦芜M(jìn)程而言钞啸,只要保證內(nèi)存緩存正確就能保證運(yùn)行時(shí)數(shù)據(jù)的正確性几蜻,而持久化,不必太及時(shí)体斩,這種手段在Android中使用還是很常見的梭稚,比如權(quán)限的更新也是這樣,況且絮吵,Google并不希望SharePreferences用于多進(jìn)程弧烤,因?yàn)椴话踩窒驴ㄒ幌耡pply與commit的區(qū)別
public void apply() {
<!--添加到內(nèi)存-->
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);
}
};
<!--延遲寫入到xml文件-->
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
<!--通知數(shù)據(jù)變化-->
notifyListeners(mcr);
}
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)用commitToMemory蹬敲,將更改提交到內(nèi)存暇昂,在這一點(diǎn)上兩者是一致的,之后又都調(diào)用了enqueueDiskWrite進(jìn)行數(shù)據(jù)持久化任務(wù)伴嗡,不過commit函數(shù)一般會(huì)在當(dāng)前線程直接寫文件急波,而apply則提交一個(gè)事務(wù)到已給線程池,之后直接返回瘪校,實(shí)現(xiàn)如下:
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr);
}
synchronized (SharedPreferencesImpl.this) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
final boolean isFromSyncCommit = (postWriteRunnable == null);
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (SharedPreferencesImpl.this) {
wasEmpty = mDiskWritesInFlight == 1;
}
<!--如果沒有其他線程在寫文件澄暮,直接在當(dāng)前線程執(zhí)行-->
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
不過如果有線程在寫文件,那么就不能直接寫阱扬,這個(gè)時(shí)候就跟apply函數(shù)一致了泣懊,但是,如果直觀說兩者的區(qū)別的話麻惶,直接說commit同步嗅定,而apply異步應(yīng)該也是沒有多大問題的。
SharePreferences多進(jìn)程使用問題
SharePreferences在新建的有個(gè)mode參數(shù)用踩,可以指定它的加載模式渠退,MODE_MULTI_PROCESS是Google提供的一個(gè)多進(jìn)程模式,但是這種模式并不是我們說的支持多進(jìn)程同步更新等脐彩,它的作用只會(huì)在getSharedPreferences的時(shí)候碎乃,才會(huì)重新從xml重加載,如果我們在一個(gè)進(jìn)程中更新xml惠奸,但是沒有通知另一個(gè)進(jìn)程梅誓,那么另一個(gè)進(jìn)程的SharePreferences是不會(huì)自動(dòng)更新的。
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
SharedPreferencesImpl 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;
}
也就是說MODE_MULTI_PROCESS只是個(gè)雞肋Flag,對于多進(jìn)程的支持幾乎為0梗掰,下面是Google文檔嵌言,簡而言之,就是:不要用及穗。
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 ContentProvider摧茴。
響應(yīng)的Google為多進(jìn)程提供了一個(gè)數(shù)據(jù)同步互斥方案,那就是基于Binder實(shí)現(xiàn)的ContentProvider埂陆,關(guān)于ContentProvider后文分析苛白。
總結(jié)
- SharePreferences是Android基于xml實(shí)現(xiàn)的一種數(shù)據(jù)持久話手段
- SharePreferences不支持多進(jìn)程
- SharePreferences的commit與apply一個(gè)是同步一個(gè)是異步(大部分場景下)
- 不要使用SharePreferences存儲太大的數(shù)據(jù)
作者:看書的小蝸牛
原文鏈接:SharePreference原理及跨進(jìn)程數(shù)據(jù)共享的問題
僅供參考,歡迎指正