轉(zhuǎn)載自:https://blog.csdn.net/yanbober/article/details/47866369
1 前言
在我們開發(fā)Android過程中數(shù)據(jù)的存儲會有很多種解決方案,譬如常見的文件存儲、數(shù)據(jù)庫存儲、網(wǎng)絡云存儲等,但是Android系統(tǒng)為咱們提供了更加方便的一種數(shù)據(jù)存儲方式,那就是SharePreference數(shù)據(jù)存儲。其實質(zhì)也就是文件存儲,只不過是符合XML標準的文件存儲而已,而且其也是Android中比較常用的簡易型數(shù)據(jù)存儲解決方案。
我們在這里不僅要探討SharePreference如何使用,還要探討其源碼是如何實現(xiàn)的;同時還要在下一篇博客討論由SharePreference衍生出來的Preference相關Android組件實現(xiàn),不過有意思的是我前幾天在網(wǎng)上看見有人對google的Preference有很大爭議,有人說他就是雞肋,丑而不靈活自定義,有人說他是一個標準,很符合設計思想,至于誰說的有道理,我想看完本文和下一篇文章你自然會有自己的觀點看法的,還有一點就是關于使用SharePreference耗時問題也是一個爭議,分析完再說吧,那就現(xiàn)在開始分析吧(基于API 22源碼)。
2 SharePreferences基本使用實例
在Android提供的幾種數(shù)據(jù)存儲方式中SharePreference屬于輕量級的鍵值存儲方式,以XML文件方式保存數(shù)據(jù),通常用來存儲一些用戶行為開關狀態(tài)等,也就是說SharePreference一般的存儲類型都是一些常見的數(shù)據(jù)類型(PS:當然也可以存儲一些復雜對象,不過需要曲線救國,下面會給出存儲復雜對象的解決方案的)。
在我們平時應用開發(fā)時或多或少都會用到SharePreference,這里就先給出一個常見的使用實例,具體如下:
public class MainActivity extends ActionBarActivity {
private SharedPreferences mSharedPreferences;
private SharedPreferences mSharedPreferencesContext;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initTest();
}
private void initTest() {
mSharedPreferencesContext = getSharedPreferences("Test", MODE_PRIVATE);
mSharedPreferences = getPreferences(MODE_PRIVATE);
SharedPreferences.Editor editor = mSharedPreferencesContext.edit();
editor.putBoolean("saveed", true);
Set<String> set = new HashSet<>();
set.add("aaaaa");
set.add("bbbbbbbb");
editor.putStringSet("content", set);
editor.commit();
SharedPreferences.Editor editorActivity = mSharedPreferences.edit();
editorActivity.putString("name", "haha");
editorActivity.commit();
}
}
運行之后adb進入data應用包下的shared_prefs目錄可以看見如下結(jié)果:
-rw-rw---- u0_a84 u0_a84 108 2015-08-23 10:34 MainActivity.xml
-rw-rw---- u0_a84 u0_a84 214 2015-08-23 10:34 Test.xml
其內(nèi)容分別如下:
at Test.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<boolean name="saveed" value="true" />
<set name="content">
<string>aaaaa</string>
<string>bbbbbbbb</string>
</set>
</map>
at MainActivity.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="name">haha</string>
</map>
可以看見SharePreference的使用還是非常簡單easy的,所以不做太多的使用說明,我們接下來重點依然是關注其實現(xiàn)原理。
3 SharePreferences源碼分析
3-1 從SharePreferences接口說起
其實講句實話,SharePreference的源碼沒啥深奧的東東,其實質(zhì)和ACache類似,都算時比較獨立的東東。分析之前我們還是先來看下SharePreference這個類的源碼,具體如下:
//你會發(fā)現(xiàn)SharedPreferences其實是一個接口而已
public interface SharedPreferences {
//定義一個用于在數(shù)據(jù)發(fā)生改變時調(diào)用的監(jiān)聽回調(diào)
public interface OnSharedPreferenceChangeListener {
//哪個key對應的值發(fā)生變化
void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);
}
//編輯SharedPreferences對象設定值的接口
public interface Editor {
//一些編輯存儲基本數(shù)據(jù)key-value的接口方法
Editor putString(String key, String value);
Editor putStringSet(String key, Set<String> values);
Editor putInt(String key, int value);
Editor putLong(String key, long value);
Editor putFloat(String key, float value);
Editor putBoolean(String key, boolean value);
//刪除指定key的鍵值對
Editor remove(String key);
//清空所有鍵值對
Editor clear();
//同步的提交到硬件磁盤
boolean commit();
//將修改數(shù)據(jù)原子提交到內(nèi)存,而后異步提交到硬件磁盤
void apply();
}
//獲取指定數(shù)據(jù)
Map<String, ?> getAll();
String getString(String key, String defValue);
Set<String> getStringSet(String key, Set<String> defValues);
int getInt(String key, int defValue);
long getLong(String key, long defValue);
float getFloat(String key, float defValue);
boolean getBoolean(String key, boolean defValue);
boolean contains(String key);
//針對preferences創(chuàng)建一個新的Editor對象,通過它你可以修改preferences里的數(shù)據(jù),并且原子化的將這些數(shù)據(jù)提交回SharedPreferences對象
Editor edit();
//注冊一個回調(diào)函數(shù),當一個preference發(fā)生變化時調(diào)用
void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
//注銷一個之前(注冊)的回調(diào)函數(shù)
void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
}
很明顯的可以看見,SharePreference源碼其實是很簡單的。既然這里說了SharePreference類只是一個接口,那么他一定有自己的實現(xiàn)類的,怎么辦呢?我們繼續(xù)往下看。
3-2 SharePreferences實現(xiàn)類SharePreferencesImpl分析
我們從上面SharePreference的使用入口可以分析,具體可以知道SharePreference的實例獲取可以通過兩種方式獲取,一種是Activity的getPreferences方法,一種是Context的getSharedPreferences方法。所以我們?nèi)缦孪葋砜聪逻@兩個方法的源碼。
先來看下Activity的getPreferences方法源碼,如下:
public SharedPreferences getPreferences(int mode) {
return getSharedPreferences(getLocalClassName(), mode);
}
哎?可以發(fā)現(xiàn),其實Activity的SharePreference實例獲取方法只是對Context的getSharedPreferences再一次封裝而已,使用getPreferences方法獲取實例默認生成的xml文件名字是當前activity類名而已。既然這樣那我們還是轉(zhuǎn)戰(zhàn)Context(其實現(xiàn)在ContextImpl中,至于不清楚Context與ContextImpl及Activity關系的請先看這篇博文,點我迅速腦補)的getSharedPreferences方法,具體如下:
//ContextImpl類中的靜態(tài)Map聲明,全局的一個sSharedPrefs
private static ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>> sSharedPrefs;
//獲取SharedPreferences實例對象
public SharedPreferences getSharedPreferences(String name, int mode) {
//SharedPreferences的實現(xiàn)類對象引用聲明
SharedPreferencesImpl sp;
//通過ContextImpl保證同步操作
synchronized (ContextImpl.class) {
if (sSharedPrefs == null) {
//實例化對象為一個復合Map,key-package,value-map
sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
}
//獲取當前應用包名
final String packageName = getPackageName();
//通過包名找到與之關聯(lián)的prefs集合packagePrefs
ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
//懶漢模式實例化
if (packagePrefs == null) {
//如果沒找到就new一個包的prefs,其實就是一個文件名對應一個SharedPreferencesImpl,可以有多個對應,所以用map
packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();
//以包名為key,實例化的所有文件map作為value添加到sSharedPrefs
sSharedPrefs.put(packageName, packagePrefs);
}
if (mPackageInfo.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
if (name == null) {
//nice處理,name傳null時用"null"代替
name = "null";
}
}
//找出與文件名name關聯(lián)的sp對象
sp = packagePrefs.get(name);
if (sp == null) {
//如果沒找到則先根據(jù)name構(gòu)建一個File的prefsFile對象
File prefsFile = getSharedPrefsFile(name);
//依據(jù)上面的File對象創(chuàng)建一個SharedPreferencesImpl對象的實例
sp = new SharedPreferencesImpl(prefsFile, mode);
//以key-value方式添加到packagePrefs中
packagePrefs.put(name, sp);
返回與name相關的SharedPreferencesImpl對象
return sp;
}
}
//如果不是第一次,則在3.0之前(默認具備該mode)或者mode為MULTI_PROCESS時調(diào)用reload方法
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
//重新加載文件數(shù)據(jù)
sp.startReloadIfChangedUnexpectedly();
}
//返回SharedPreferences實例對象sp
return sp;
}
我們可以發(fā)現(xiàn),上面方法中首先調(diào)運了getSharedPrefsFile來獲取一個File對象,所以我們繼續(xù)先來看下這個方法,具體如下:
public File getSharedPrefsFile(String name) {
//依據(jù)我們傳入的文件名字符串創(chuàng)建一個后綴為xml的文件
return makeFilename(getPreferencesDir(), name + ".xml");
}
private File getPreferencesDir() {
synchronized (mSync) {
if (mPreferencesDir == null) {
//獲取當前app的data目錄下的shared_prefs目錄
mPreferencesDir = new File(getDataDirFile(), "shared_prefs");
}
return mPreferencesDir;
}
}
可以看見,原來SharePreference文件存儲路徑和文件創(chuàng)建是這個來的。繼續(xù)往下看可以發(fā)現(xiàn)接著調(diào)運了SharedPreferencesImpl的構(gòu)造函數(shù),至于這個構(gòu)造函數(shù)用來干嘛,下面會分析。
好了,到這里我們先回過頭稍微總結(jié)一下目前的源碼分析結(jié)論,具體如下:
前面我們有文章分析了Android中的Context,這里又發(fā)現(xiàn)ContextImpl中有一個靜態(tài)的ArrayMap變量sSharedPrefs。這時候你想到了啥呢?無論有多少個ContextImpl對象實例,系統(tǒng)都共享這一個sSharedPrefs的Map,應用啟動以后首次使用SharePreference時創(chuàng)建,系統(tǒng)結(jié)束時才可能會被垃圾回收器回收,所以如果我們一個App中頻繁的使用不同文件名的SharedPreferences很多時這個Map就會很大,也即會占用移動設備寶貴的內(nèi)存空間,所以說我們應用中應該盡可能少的使用不同文件名的SharedPreferences,取而代之的是合并他們,減小內(nèi)存使用。同時上面最后一段代碼也及具有隱藏含義,其表明了SharedPreferences是可以通過MODE_MULTI_PROCESS來進行夸進程訪問文件數(shù)據(jù)的,其reload就是為了夸進程能更好的刷新訪問數(shù)據(jù)。
好了,還記不記得上面我們分析留的尾巴呢?現(xiàn)在我們就來看看這個尾巴,可以發(fā)現(xiàn)SharedPreferencesImpl類其實就是SharedPreferences接口的實現(xiàn)類,其構(gòu)造函數(shù)如下:
final class SharedPreferencesImpl implements SharedPreferences {
......
//構(gòu)造函數(shù),file是前面分析data目錄下創(chuàng)建的傳入name的xml文件,mode為傳入的訪問方式
SharedPreferencesImpl(File file, int mode) {
mFile = file;
//依據(jù)文件名創(chuàng)建一個同名的.bak備份文件,當mFile出現(xiàn)crash的會用mBackupFile來替換恢復數(shù)據(jù)
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
//將文件從flash或者sdcard異步加載到內(nèi)存中
startLoadFromDisk();
}
......
//創(chuàng)建同名備份文件
private static File makeBackupFile(File prefsFile) {
return new File(prefsFile.getPath() + ".bak");
}
......
private void startLoadFromDisk() {
//同步操作mLoaded標志,寫為未加載,這貨是關鍵的關鍵!?。?!
synchronized (this) {
mLoaded = false;
}
//開啟一個線程異步同步加載disk文件到內(nèi)存
new Thread("SharedPreferencesImpl-load") {
public void run() {
synchronized (SharedPreferencesImpl.this) {
//新線程中在SharedPreferencesImpl對象鎖中異步load數(shù)據(jù),如果此時數(shù)據(jù)還未load完成,則其他線程調(diào)用SharedPreferences.getXXX方法都會被阻塞,具體原因關注mLoaded標志變量即可?。。。。? loadFromDiskLocked();
}
}
}.start();
}
}
好了,到這里你會發(fā)現(xiàn)整個SharedPreferencesImpl的構(gòu)造函數(shù)很簡單,那我們就繼續(xù)分析真正的異步加載文件到內(nèi)存過程,如下:
private void loadFromDiskLocked() {
//如果已經(jīng)異步加載直接return返回
if (mLoaded) {
return;
}
//如果存在備份文件則直接使用備份文件
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
......
Map map = null;
StructStat stat = null;
try {
//獲取Linux文件stat信息,Linux高級C中經(jīng)常出現(xiàn)的
stat = Os.stat(mFile.getPath());
//文件至少是可讀的
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
//把文件以BufferedInputStream流讀出來
str = new BufferedInputStream(
new FileInputStream(mFile), 16*1024);
//使用系統(tǒng)提供的XmlUtils工具類將xml流解析轉(zhuǎn)換為map類型數(shù)據(jù)
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) {
//把解析的map賦值給mMap
mMap = map;
mStatTimestamp = stat.st_mtime;//記錄時間戳
mStatSize = stat.st_size;//記錄文件大小
} else {
mMap = new HashMap<String, Object>();
}
//喚醒其他等待線程(其實就是調(diào)運該類的getXXX方法的線程),因為在getXXX時會通過mLoaded標記是否進入wait,所以這里需要notify
notifyAll();
}
OK,到此整個Android應用獲取SharePreference實例的過程我們就分析完了,簡單總結(jié)下如下:
創(chuàng)建相關權(quán)限和mode的xml文件,異步同步鎖加載xml文件并解析xml數(shù)據(jù)為map類型到內(nèi)存中等待使用操作,特別注意,在xml文件異步加載未完成時調(diào)運SharePreference的getXXX及setXXX方法是阻塞等待的。由此也可以知道,一旦拿到SharePreference對象之后的getXXX操作其實都不再是文件讀操作了,也就不存在網(wǎng)上扯蛋的認為多次頻繁使用getXXX方法降低性能的說法了。
分析完了構(gòu)造實例化,我們回憶可以知道使用SharePreference可以通過getXXX方法直接獲取已經(jīng)存在的key-value數(shù)據(jù),下面我們就來看下這個過程,這里我們隨意看一個方法即可,如下:
public boolean getBoolean(String key, boolean defValue) {
//可以看見,和上面異步load數(shù)據(jù)使用的是同一個對象鎖
synchronized (this) {
//阻塞等待異步加載線程加載完成notify
awaitLoadedLocked();
//加載完成后解析的xml數(shù)據(jù)放在mMap對象中,我們從mMap中找出指定key的數(shù)據(jù)
Boolean v = (Boolean)mMap.get(key);
//存在返回找到的值,不存在返回設置的defValue
return v != null ? v : defValue;
}
}
先不解釋,我們來關注下上面方法調(diào)運的awaitLoadedLocked方法,具體如下:
private void awaitLoadedLocked() {
......
//核心,這就是異步阻塞等待
while (!mLoaded) {
try {
wait();
} catch (InterruptedException unused) {
}
}
}
哈哈,不解釋,這也太赤裸裸的明顯了,就是阻塞,就是這么任性,沒轍。那我們繼續(xù)攻占高地唄,get完事了,那就是set了呀。
3-3 SharePreferences內(nèi)部類Editor實現(xiàn)EditorImpl分析
還記不記得set是在SharePreference接口的Editor接口中定義的,而SharePreference提供了edit()方法來獲取Editor實例,我們先來看下這個edit()方法吧,如下:
public Editor edit() {
//握草!這也和異步load用的一把鎖
synchronized (this) {
//阻塞等待,不解釋吧,向上看。。。
awaitLoadedLocked();
}
//異步加載OK以后通過EditorImpl創(chuàng)建Editor實例
return new EditorImpl();
}
可以看見,SharePreference的edit()方法其實就是阻塞等待返回一個Editor的實例(Editor的實現(xiàn)是EditorImpl),那我們就順藤摸瓜一把,來看下這個EditorImpl這個類,如下:
public final class EditorImpl implements Editor {
//創(chuàng)建一個mModified的key-value集合,用來在內(nèi)存中暫存數(shù)據(jù)
private final Map<String, Object> mModified = Maps.newHashMap();
//一個是否清除preference的flag
private boolean mClear = false;
......//省略類似的putXXX方法
public Editor putBoolean(String key, boolean value) {
//同步鎖操作
synchronized (this) {
//將我們要存儲的數(shù)據(jù)放入mModified集合中
mModified.put(key, value);
//返回當前對象實例,方便這種模式的代碼寫法:putXXX().putXXX();
return this;
}
}
//不用過多解釋,同步刪除mModified中包含key的數(shù)據(jù)
public Editor remove(String key) {
synchronized (this) {
mModified.put(key, this);
return this;
}
}
//不解釋,要清楚所有數(shù)據(jù)則直接置位mClear標記
public Editor clear() {
synchronized (this) {
mClear = true;
return this;
}
}
......
}
好了,到此你可以發(fā)現(xiàn)Editor的setXXX及clear操作僅僅只是將相關數(shù)據(jù)暫存到內(nèi)存中或者設置好標記為,也就是說調(diào)運了Editor的putXXX后其實數(shù)據(jù)是沒有存入SharePreference的。那么通過我們一開始的實例可以知道,要想將Editor的數(shù)據(jù)存入SharePreference文件需要調(diào)運Editor的commit或者apply方法來生效。所以我們接下來先來看看Editor類常用的commit方法實現(xiàn)原理,如下:
public boolean commit() {
//1.先通過commitToMemory方法提交到內(nèi)存
MemoryCommitResult mcr = commitToMemory();
//2.寫文件操作
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
//阻塞等待寫操作完成,UI操作需要注意?。?!所以如果不關心返回值可以考慮用apply替代,具體原因等會分析apply就明白了。
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
//3.通知數(shù)據(jù)發(fā)生變化了
notifyListeners(mcr);
//4.返回寫文件是否成功狀態(tài)
return mcr.writeToDiskResult;
}
我去,小小一個commit方法做了這么多操作,主要分為四個步驟,我們先來看下第一個步驟,通過commitToMemory方法提交到內(nèi)存返回一個MemoryCommitResult對象。分析commitToMemory方法前先看下MemoryCommitResult這個類,具體如下:
// Return value from EditorImpl#commitToMemory()
//也是內(nèi)部類,只是為了組織數(shù)據(jù)結(jié)構(gòu)而誕生,也就是EditorImpl.commitToMemory()的返回值
private static class MemoryCommitResult {
public boolean changesMade; // any keys different?
public List<String> keysModified; // may be null
public Set<android.content.SharedPreferences.OnSharedPreferenceChangeListener> listeners; // may be null
public Map<?, ?> mapToWriteToDisk;
public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
public volatile boolean writeToDiskResult = false;
public void setDiskWriteResult(boolean result) {
writeToDiskResult = result;
writtenToDiskLatch.countDown();
}
}
回過頭現(xiàn)在來看commitToMemory方法,具體如下:
// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
//啥也不說,先整一個實例化對象
MemoryCommitResult mcr = new MemoryCommitResult();
//和SharedPreferencesImpl共用一把鎖
synchronized (SharedPreferencesImpl.this) {
// 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);
}
//構(gòu)造數(shù)據(jù)結(jié)構(gòu),把通過SharedPreferencesImpl構(gòu)造函數(shù)里異步加載的文件xml解析結(jié)果mMap賦值給要寫到disk的Map
mcr.mapToWriteToDisk = mMap;
//增加一個未完成的寫opt
mDiskWritesInFlight++;
//判斷有沒有監(jiān)聽設置
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
//創(chuàng)建監(jiān)聽隊列
mcr.keysModified = new ArrayList<String>();
mcr.listeners =
new HashSet<android.content.SharedPreferences.OnSharedPreferenceChangeListener>(mListeners.keySet());
}
//再加一把自己的鎖
synchronized (this) {
//如果調(diào)運的是Editor的clear方法,則這里commit時這么處理
if (mClear) {
//如果從文件里加載出來的xml不為空
if (!mMap.isEmpty()) {
//設置數(shù)據(jù)結(jié)構(gòu)中數(shù)據(jù)變化標志為true
mcr.changesMade = true;
//清空內(nèi)存中xml數(shù)據(jù)
mMap.clear();
}
//處理完畢,標記復位,程序繼續(xù)執(zhí)行,所以如果這次Editor中如果有寫數(shù)據(jù)且還未commit,則執(zhí)行完這次commit之后不會清掉本次寫操作的數(shù)據(jù),只會clear以前xml文件中的所有數(shù)據(jù)
mClear = false;
}
//mModified是調(diào)運Editor的setXXX零時存儲的map
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.
//刪除需要刪除的key-value
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;
}
}
//把變化和新加的數(shù)據(jù)更新到SharePreferenceImpl的mMap中
mMap.put(k, v);
}
//設置數(shù)據(jù)結(jié)構(gòu)變化標記
mcr.changesMade = true;
if (hasListeners) {
//設置監(jiān)聽
mcr.keysModified.add(k);
}
}
//清空Editor中零時存儲的數(shù)據(jù)
mModified.clear();
}
}
//返回重新更新過mMap值封裝的數(shù)據(jù)結(jié)構(gòu)
return mcr;
}
到此我們Editor的commit方法的第一步已經(jīng)完成,根據(jù)寫操作組織內(nèi)存數(shù)據(jù),返回組織后的數(shù)據(jù)結(jié)構(gòu)。接下來我們繼續(xù)回到commit方法看下第二步—-寫到文件中,其核心是調(diào)運SharedPreferencesImpl類的enqueueDiskWrite方法實現(xiàn)。具體如下:
//按照隊列把內(nèi)存數(shù)據(jù)寫入磁盤,commit時postWriteRunnable為null,apply時不為null
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
//創(chuàng)建一個writeToDiskRunnable的Runnable對象
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
//真正的寫文件操作
writeToFile(mcr);
}
synchronized (SharedPreferencesImpl.this) {
//寫完一個計數(shù)器-1
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
//等會apply分析
postWriteRunnable.run();
}
}
};
//判斷是同步寫還是異步
final boolean isFromSyncCommit = (postWriteRunnable == null);
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
//commit方式走這里
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (SharedPreferencesImpl.this) {
//如果當前只有一個寫操作
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
//一個寫操作就直接在當前線程中寫文件,不用另起線程
writeToDiskRunnable.run();
//寫完文件就返回
return;
}
}
//如果是apply就在線程池中執(zhí)行
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
可以發(fā)現(xiàn),commit從內(nèi)存寫文件是在當前調(diào)運線程中直接執(zhí)行的。那我們再來看看這個寫內(nèi)存到磁盤方法中真正的寫方法writeToFile,如下:
// Note: must hold mWritingToDiskLock
private void writeToFile(MemoryCommitResult mcr) {
if (mFile.exists()) {
if (!mcr.changesMade) {
//如果文件存在且沒有改變的數(shù)據(jù)則直接返回寫OK
mcr.setDiskWriteResult(true);
return;
}
if (!mBackupFile.exists()) {
//如果要寫入的文件已經(jīng)存在,并且備份文件不存在時就先把當前文件備份一份,因為如果本次寫操作失敗時數(shù)據(jù)可能已經(jīng)亂了,所以下次實例化load數(shù)據(jù)時可以從備份文件中恢復
if (!mFile.renameTo(mBackupFile)) {
Log.e(TAG, "Couldn't rename file " + mFile
+ " to backup file " + mBackupFile);
//命名失敗直接返回寫失敗了
mcr.setDiskWriteResult(false);
return;
}
} else {
//備份文件存在就把源文件刪掉,因為要寫新的
mFile.delete();
}
}
try {
//創(chuàng)建mFile文件
FileOutputStream str = createFileOutputStream(mFile);
if (str == null) {
//創(chuàng)建失敗直接返回寫失敗了
mcr.setDiskWriteResult(false);
return;
}
//把數(shù)據(jù)寫入mFile文件
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
//徹底同步到磁盤文件中
FileUtils.sync(str);
str.close();
//設置文件權(quán)限mode
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
//和剛開始實例化load時一樣,更新文件時間戳和大小
try {
final StructStat stat = Os.stat(mFile.getPath());
synchronized (this) {
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
}
} catch (ErrnoException e) {
// Do nothing
}
// Writing was successful, delete the backup file if there is one.
//寫成功了mFile那就把備份文件直接刪掉,沒用了。
mBackupFile.delete();
//設置寫成功了,然后返回
mcr.setDiskWriteResult(true);
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()) {
//上面如果出錯了就刪掉,因為寫之前已經(jīng)備份過數(shù)據(jù)了,下次load時load備份數(shù)據(jù)
if (!mFile.delete()) {
Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
}
}
//寫失敗了
mcr.setDiskWriteResult(false);
}
回過頭可以發(fā)現(xiàn),上面commit的第二步寫磁盤操作其實是做了類似數(shù)據(jù)庫的事務操作機制的(備份文件)。接著可以繼續(xù)分析commit方法的第三四步,很明顯可以看出,第三步就是回調(diào)設置的監(jiān)聽方法,通知數(shù)據(jù)變化了,第四步就是返回commit寫文件是否成功。
總體到這里你可以發(fā)現(xiàn),一個常用的SharePreferences過程已經(jīng)完全分析完畢。接下來我們就再簡單說說Editor的apply方法原理,先來看下Editor的apply方法,如下:
public void apply() {
//有了上面commit分析,這個雷同,寫數(shù)據(jù)到內(nèi)存,返回數(shù)據(jù)結(jié)構(gòu)
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
//等待寫文件結(jié)束
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
QueuedWork.add(awaitCommit);
//一個收尾的Runnable
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.remove(awaitCommit);
}
};
//這個上面commit已經(jīng)分析過的,這里postWriteRunnable不為null,所以會在一個新的線程池調(diào)運postWriteRunnable的run方法
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);
}
看到了吧,其實和commit類似,只不過他是異步寫的,沒在當前線程執(zhí)行寫文件操作,還有就是他不像commit一樣返回文件是否寫成功狀態(tài)。
3-4 SharePreferences源碼分析總結(jié)
題外趣事: 記得好像是去年有一次我用SharePreferences存儲了幾個boolean值,由于開發(fā)調(diào)試,當時我直接進入系統(tǒng)data目錄下應用的xml存儲文件夾,然后執(zhí)行了刪除操作;接著我沒有重啟應用,直接打斷點調(diào)試,握草!奇跡的發(fā)現(xiàn)SharePreferences調(diào)運get時竟然拿到了不是初始化的值。哈哈,其實就是上面分析的,加載完后是一個靜態(tài)的map,進程沒掛之前一直用的內(nèi)存數(shù)據(jù)。
通過上面的實例及源碼分析可以發(fā)現(xiàn):
SharedPreferences在實例化時首先會從sdcard異步讀文件,然后緩存在內(nèi)存中;接下來的讀操作都是內(nèi)存緩存操作而不是文件操作。
在SharedPreferences的Editor中如果用commit()方法提交數(shù)據(jù),其過程是先把數(shù)據(jù)更新到內(nèi)存,然后在當前線程中寫文件操作,提交完成返回提交狀態(tài);如果用的是apply()方法提交數(shù)據(jù),首先也是寫到內(nèi)存,接著在一個新線程中異步寫文件,然后沒有返回值。
由于上面分析了,在寫操作commit時有三級鎖操作,所以效率很低,所以當我們一次有多個修改寫操作時等都批量put完了再一次提交確認,這樣可以提高效率。
可以發(fā)現(xiàn),在簡單數(shù)據(jù)行為狀態(tài)存儲中,Android的SharedPreferences是一個安全而且不錯的選擇。
4 SharePreferences進階項目解決方案
其實分析完源碼之后也就差不多了。這里所謂的進階項目解決方案只是曲線救國的2B行為,只是表明還有這么一種方案,至于項目中是否值得提倡那就要綜合酌情考慮了。
4-1 SharePreferences存儲復雜對象的解決案例
這個案例完全有些多余(因為看完這個例子你會發(fā)現(xiàn)還不如使用github上大神流行的ACache更爽呢?。且脖容^有意思,所以還是拿出來說說,其實網(wǎng)上實現(xiàn)的也很多。
我們有時候可能會涉及到存儲一個自定義對象到SharedPreferences中,這個怎么實現(xiàn)呢?標準的SharedPreferences的Editor只提供幾個常見類型的put方法呀,其實可以實現(xiàn)的,原理就是Base64轉(zhuǎn)碼為字符串存儲,如下給出我的一個工具類,在項目中可以直接使用:
/**
* @version 1.0
* http://blog.csdn.net/yanbober
* 保存任意類型對象到SharedPreferences工具類
*/
public final class ObjectSharedPreferences {
private Context mContext;
private String mName;
private int mMode;
public ObjectSharedPreferences(Context context, String name) {
this(context, name, Context.MODE_PRIVATE);
}
public ObjectSharedPreferences(Context context, String name, int mode) {
this.mContext = context;
this.mName = name;
this.mMode = mode;
}
/**
* 保存任意object對象到SharedPreferences
* @param key
* @param object
*/
public void setObject(String key, Object object) {
SharedPreferences preferences = mContext.getSharedPreferences(mName, mMode);
ObjectOutputStream objOutputStream = null;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
objOutputStream = new ObjectOutputStream(outputStream);
objOutputStream.writeObject(object);
String objectVal = new String(Base64.encode(outputStream.toByteArray(), Base64.DEFAULT));
SharedPreferences.Editor editor = preferences.edit();
editor.putString(key, objectVal);
editor.commit();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (outputStream != null) {
outputStream.close();
}
if (objOutputStream != null) {
objOutputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 從SharedPreferences獲取任意object對象
* @param key
* @param clazz
* @return
*/
public <T> T getObject(String key, Class<T> clazz) {
SharedPreferences preferences = this.mContext.getSharedPreferences(mName, mMode);
if (preferences.contains(key)) {
String objectVal = preferences.getString(key, null);
byte[] buffer = Base64.decode(objectVal, Base64.DEFAULT);
ByteArrayInputStream inputStream = new ByteArrayInputStream(buffer);
ObjectInputStream objInputStream = null;
try {
objInputStream = new ObjectInputStream(inputStream);
return (T) objInputStream.readObject();
} catch (StreamCorruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
if (objInputStream != null) {
objInputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
}
好了,沒啥解釋的,依據(jù)需求自己決定吧,這只是一種方案而已,其替代方案很多。
4-2 SharePreferences跨進程訪問解決案例
Android系統(tǒng)有自己的一套安全機制,當應用程序在安裝時系統(tǒng)會分配給他們一個唯一的userid,當該應用程序需要訪問文件等資源的時候必須要匹配userid。默認情況下安卓應用程序創(chuàng)建的各種文件(SharePreferences、數(shù)據(jù)庫等)都是私有的(在/data/data/[APP PACKAGE NAME]/files),其他程序無法訪問。在3.0以后必須在創(chuàng)建文件時顯示的指定了Context.MODE_WORLD_READABLE或者Context.MODE_WORLD_WRITEABLE才能被其他進程(應用)訪問。
這個案例也就類似于Android的ContentProvider,可以跨進程訪問,但是其原理是給權(quán)限然后多進程文件訪問而已。具體我們看如下一個案例,一個用來類比當服務端,一個用來類比當客戶端,如下:
服務端:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private TextView mTextView;
private EditText mEditText;
private Button mButton;
private SharedPreferences mPreferences;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView) findViewById(R.id.content);
mEditText = (EditText) findViewById(R.id.input);
mButton = (Button) findViewById(R.id.click);
mButton.setOnClickListener(this);
//操作當前APP本地的SharedPreferences文件local.xml
mPreferences = getSharedPreferences("local", MODE_WORLD_READABLE | MODE_WORLD_WRITEABLE);
}
@Override
public void onClick(View v) {
mTextView.setText(mPreferences.getString("input", "default"));
SharedPreferences.Editor editor = mPreferences.edit();
editor.putString("input", mEditText.getText().toString());
editor.commit();
}
}
客戶端:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private TextView mTextView;
private EditText mEditText;
private Button mButton;
private SharedPreferences mPreferences;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView) findViewById(R.id.content);
mEditText = (EditText) findViewById(R.id.input);
mButton = (Button) findViewById(R.id.click);
mButton.setOnClickListener(this);
mPreferences = getRemotePreferences("com.local.localapp", "local");
}
//獲取其他APP的SharedPreferences文件local.xml,特別注意MODE_MULTI_PROCESS的mode
private SharedPreferences getRemotePreferences(String pkg, String file) {
try {
Context remote = createPackageContext(pkg, Context.CONTEXT_IGNORE_SECURITY);
return remote.getSharedPreferences(file, MODE_WORLD_READABLE | MODE_WORLD_WRITEABLE | MODE_MULTI_PROCESS);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return null;
}
@Override
public void onClick(View v) {
mTextView.setText(mPreferences.getString("input", "default"));
SharedPreferences.Editor editor = mPreferences.edit();
editor.putString("input", mEditText.getText().toString());
editor.commit();
}
}
不解釋,需求自己看情況吧,這就是一個經(jīng)典的跨進程訪問SharedPreferences,主要原理就是設置flag然后獲取其他App的Context。
5 SharePreferences總結(jié)
到此整個SharePreferences的使用及原理和進階開發(fā)案例都已經(jīng)全面剖析完畢,相信你對Android的SharePreferences會有一個全新的自己的認識,不再會被網(wǎng)上那些爭議的評論而左右。下一篇文章會繼續(xù)探討Android的SharePreferences拓展控件,即Preference控件家族的使用及源碼分析。