Android應用Preference相關及原始碼淺析(SharePreferences篇)
1 前言
在我們開發Android過程中資料的儲存會有很多種解決方案,譬如常見的檔案儲存、資料庫儲存、網路雲端儲存等,但是Android系統為咱們提供了更加方便的一種資料儲存方式,那就是SharePreference資料儲存。其實質也就是檔案儲存,只不過是符合XML標準的檔案儲存而已,而且其也是Android中比較常用的簡易型資料儲存解決方案。
我們在這裡不僅要探討SharePreference如何使用,還要探討其原始碼是如何實現的;同時還要在下一篇部落格討論由SharePreference衍生出來的Preference相關Android元件實現,不過有意思的是我前幾天在網上看見有人對google的Preference有很大爭議,有人說他就是雞肋,醜而不靈活自定義,有人說他是一個標準,很符合設計思想,至於誰說的有道理,我想看完本文和下一篇文章你自然會有自己的觀點看法的,還有一點就是關於使用SharePreference耗時問題也是一個爭議,分析完再說吧,那就現在開始分析吧(基於API 22原始碼)。
2 SharePreferences基本使用例項
在Android提供的幾種資料儲存方式中SharePreference屬於輕量級的鍵值儲存方式,以XML檔案方式儲存資料,通常用來儲存一些使用者行為開關狀態等,也就是說SharePreference一般的儲存型別都是一些常見的資料型別(PS:當然也可以儲存一些複雜物件,不過需要曲線救國,下面會給出儲存複雜物件的解決方案的)。
在我們平時應用開發時或多或少都會用到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目錄可以看見如下結果:
-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
其內容分別如下:
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的,所以不做太多的使用說明,我們接下來重點依然是關注其實現原理。
3 SharePreferences原始碼分析
3-1 從SharePreferences介面說起
其實講句實話,SharePreference的原始碼沒啥深奧的東東,其實質和ACache類似,都算時比較獨立的東東。分析之前我們還是先來看下SharePreference這個類的原始碼,具體如下:
//你會發現SharedPreferences其實是一個介面而已
public interface SharedPreferences {
//定義一個用於在資料發生改變時呼叫的監聽回撥
public interface OnSharedPreferenceChangeListener {
//哪個key對應的值發生變化
void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);
}
//編輯SharedPreferences物件設定值的介面
public interface Editor {
//一些編輯儲存基本資料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();
//將修改資料原子提交到記憶體,而後非同步提交到硬體磁碟
void apply();
}
//獲取指定資料
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建立一個新的Editor物件,通過它你可以修改preferences裡的資料,並且原子化的將這些資料提交回SharedPreferences物件
Editor edit();
//註冊一個回撥函式,當一個preference發生變化時呼叫
void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
//登出一個之前(註冊)的回撥函式
void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
}
很明顯的可以看見,SharePreference原始碼其實是很簡單的。既然這裡說了SharePreference類只是一個介面,那麼他一定有自己的實現類的,怎麼辦呢?我們繼續往下看。
3-2 SharePreferences實現類SharePreferencesImpl分析
我們從上面SharePreference的使用入口可以分析,具體可以知道SharePreference的例項獲取可以通過兩種方式獲取,一種是Activity的getPreferences方法,一種是Context的getSharedPreferences方法。所以我們如下先來看下這兩個方法的原始碼。
先來看下Activity的getPreferences方法原始碼,如下:
public SharedPreferences getPreferences(int mode) {
return getSharedPreferences(getLocalClassName(), mode);
}
哎?可以發現,其實Activity的SharePreference例項獲取方法只是對Context的getSharedPreferences再一次封裝而已,使用getPreferences方法獲取例項預設生成的xml檔名字是當前activity類名而已。既然這樣那我們還是轉戰Context(其實現在ContextImpl中,至於不清楚Context與ContextImpl及Activity關係的請先看這篇博文,點我迅速腦補)的getSharedPreferences方法,具體如下:
//ContextImpl類中的靜態Map宣告,全域性的一個sSharedPrefs
private static ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>> sSharedPrefs;
//獲取SharedPreferences例項物件
public SharedPreferences getSharedPreferences(String name, int mode) {
//SharedPreferences的實現類物件引用宣告
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();
//通過包名找到與之關聯的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關聯的sp物件
sp = packagePrefs.get(name);
if (sp == null) {
//如果沒找到則先根據name構建一個File的prefsFile物件
File prefsFile = getSharedPrefsFile(name);
//依據上面的File物件建立一個SharedPreferencesImpl物件的例項
sp = new SharedPreferencesImpl(prefsFile, mode);
//以key-value方式新增到packagePrefs中
packagePrefs.put(name, sp);
返回與name相關的SharedPreferencesImpl物件
return sp;
}
}
//如果不是第一次,則在3.0之前(預設具備該mode)或者mode為MULTI_PROCESS時呼叫reload方法
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
//重新載入檔案資料
sp.startReloadIfChangedUnexpectedly();
}
//返回SharedPreferences例項物件sp
return sp;
}
我們可以發現,上面方法中首先調運了getSharedPrefsFile來獲取一個File物件,所以我們繼續先來看下這個方法,具體如下:
public File getSharedPrefsFile(String name) {
//依據我們傳入的檔名字串建立一個字尾為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檔案儲存路徑和檔案建立是這個來的。繼續往下看可以發現接著調運了SharedPreferencesImpl的建構函式,至於這個建構函式用來幹嘛,下面會分析。
好了,到這裡我們先回過頭稍微總結一下目前的原始碼分析結論,具體如下:
前面我們有文章分析了Android中的Context,這裡又發現ContextImpl中有一個靜態的ArrayMap變數sSharedPrefs。這時候你想到了啥呢?無論有多少個ContextImpl物件例項,系統都共享這一個sSharedPrefs的Map,應用啟動以後首次使用SharePreference時建立,系統結束時才可能會被垃圾回收器回收,所以如果我們一個App中頻繁的使用不同檔名的SharedPreferences很多時這個Map就會很大,也即會佔用移動裝置寶貴的記憶體空間,所以說我們應用中應該儘可能少的使用不同檔名的SharedPreferences,取而代之的是合併他們,減小記憶體使用。同時上面最後一段程式碼也及具有隱藏含義,其表明了SharedPreferences是可以通過MODE_MULTI_PROCESS來進行誇程序訪問檔案資料的,其reload就是為了誇程序能更好的重新整理訪問資料。
好了,還記不記得上面我們分析留的尾巴呢?現在我們就來看看這個尾巴,可以發現SharedPreferencesImpl類其實就是SharedPreferences介面的實現類,其建構函式如下:
final class SharedPreferencesImpl implements SharedPreferences {
......
//建構函式,file是前面分析data目錄下建立的傳入name的xml檔案,mode為傳入的訪問方式
SharedPreferencesImpl(File file, int mode) {
mFile = file;
//依據檔名建立一個同名的.bak備份檔案,當mFile出現crash的會用mBackupFile來替換恢復資料
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
//將檔案從flash或者sdcard非同步載入到記憶體中
startLoadFromDisk();
}
......
//建立同名備份檔案
private static File makeBackupFile(File prefsFile) {
return new File(prefsFile.getPath() + ".bak");
}
......
private void startLoadFromDisk() {
//同步操作mLoaded標誌,寫為未載入,這貨是關鍵的關鍵!!!!
synchronized (this) {
mLoaded = false;
}
//開啟一個執行緒非同步同步載入disk檔案到記憶體
new Thread("SharedPreferencesImpl-load") {
public void run() {
synchronized (SharedPreferencesImpl.this) {
//新執行緒中在SharedPreferencesImpl物件鎖中非同步load資料,如果此時資料還未load完成,則其他執行緒呼叫SharedPreferences.getXXX方法都會被阻塞,具體原因關注mLoaded標誌變數即可!!!!!
loadFromDiskLocked();
}
}
}.start();
}
}
好了,到這裡你會發現整個SharedPreferencesImpl的建構函式很簡單,那我們就繼續分析真正的非同步載入檔案到記憶體過程,如下:
private void loadFromDiskLocked() {
//如果已經非同步載入直接return返回
if (mLoaded) {
return;
}
//如果存在備份檔案則直接使用備份檔案
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
......
Map map = null;
StructStat stat = null;
try {
//獲取Linux檔案stat資訊,Linux高階C中經常出現的
stat = Os.stat(mFile.getPath());
//檔案至少是可讀的
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
//把檔案以BufferedInputStream流讀出來
str = new BufferedInputStream(
new FileInputStream(mFile), 16*1024);
//使用系統提供的XmlUtils工具類將xml流解析轉換為map型別資料
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>();
}
//喚醒其他等待執行緒(其實就是調運該類的getXXX方法的執行緒),因為在getXXX時會通過mLoaded標記是否進入wait,所以這裡需要notify
notifyAll();
}
OK,到此整個Android應用獲取SharePreference例項的過程我們就分析完了,簡單總結下如下:
建立相關許可權和mode的xml檔案,非同步同步鎖載入xml檔案並解析xml資料為map型別到記憶體中等待使用操作,特別注意,在xml檔案非同步載入未完成時調運SharePreference的getXXX及setXXX方法是阻塞等待的。由此也可以知道,一旦拿到SharePreference物件之後的getXXX操作其實都不再是檔案讀操作了,也就不存在網上扯蛋的認為多次頻繁使用getXXX方法降低效能的說法了。
分析完了構造例項化,我們回憶可以知道使用SharePreference可以通過getXXX方法直接獲取已經存在的key-value資料,下面我們就來看下這個過程,這裡我們隨意看一個方法即可,如下:
public boolean getBoolean(String key, boolean defValue) {
//可以看見,和上面非同步load資料使用的是同一個物件鎖
synchronized (this) {
//阻塞等待非同步載入執行緒載入完成notify
awaitLoadedLocked();
//載入完成後解析的xml資料放在mMap物件中,我們從mMap中找出指定key的資料
Boolean v = (Boolean)mMap.get(key);
//存在返回找到的值,不存在返回設定的defValue
return v != null ? v : defValue;
}
}
先不解釋,我們來關注下上面方法調運的awaitLoadedLocked方法,具體如下:
private void awaitLoadedLocked() {
......
//核心,這就是非同步阻塞等待
while (!mLoaded) {
try {
wait();
} catch (InterruptedException unused) {
}
}
}
哈哈,不解釋,這也太赤裸裸的明顯了,就是阻塞,就是這麼任性,沒轍。那我們繼續攻佔高地唄,get完事了,那就是set了呀。
3-3 SharePreferences內部類Editor實現EditorImpl分析
還記不記得set是在SharePreference介面的Editor介面中定義的,而SharePreference提供了edit()方法來獲取Editor例項,我們先來看下這個edit()方法吧,如下:
public Editor edit() {
//握草!這也和非同步load用的一把鎖
synchronized (this) {
//阻塞等待,不解釋吧,向上看。。。
awaitLoadedLocked();
}
//非同步載入OK以後通過EditorImpl建立Editor例項
return new EditorImpl();
}
可以看見,SharePreference的edit()方法其實就是阻塞等待返回一個Editor的例項(Editor的實現是EditorImpl),那我們就順藤摸瓜一把,來看下這個EditorImpl這個類,如下:
public final class EditorImpl implements Editor {
//建立一個mModified的key-value集合,用來在記憶體中暫存資料
private final Map<String, Object> mModified = Maps.newHashMap();
//一個是否清除preference的flag
private boolean mClear = false;
......//省略類似的putXXX方法
public Editor putBoolean(String key, boolean value) {
//同步鎖操作
synchronized (this) {
//將我們要儲存的資料放入mModified集合中
mModified.put(key, value);
//返回當前物件例項,方便這種模式的程式碼寫法:putXXX().putXXX();
return this;
}
}
//不用過多解釋,同步刪除mModified中包含key的資料
public Editor remove(String key) {
synchronized (this) {
mModified.put(key, this);
return this;
}
}
//不解釋,要清楚所有資料則直接置位mClear標記
public Editor clear() {
synchronized (this) {
mClear = true;
return this;
}
}
......
}
好了,到此你可以發現Editor的setXXX及clear操作僅僅只是將相關資料暫存到記憶體中或者設定好標記為,也就是說調運了Editor的putXXX後其實資料是沒有存入SharePreference的。那麼通過我們一開始的例項可以知道,要想將Editor的資料存入SharePreference檔案需要調運Editor的commit或者apply方法來生效。所以我們接下來先來看看Editor類常用的commit方法實現原理,如下:
public boolean commit() {
//1.先通過commitToMemory方法提交到記憶體
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.通知資料發生變化了
notifyListeners(mcr);
//4.返回寫檔案是否成功狀態
return mcr.writeToDiskResult;
}
我去,小小一個commit方法做了這麼多操作,主要分為四個步驟,我們先來看下第一個步驟,通過commitToMemory方法提交到記憶體返回一個MemoryCommitResult物件。分析commitToMemory方法前先看下MemoryCommitResult這個類,具體如下:
// Return value from EditorImpl#commitToMemory()
//也是內部類,只是為了組織資料結構而誕生,也就是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();
}
}
回過頭現在來看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);
}
//構造資料結構,把通過SharedPreferencesImpl建構函式裡非同步載入的檔案xml解析結果mMap賦值給要寫到disk的Map
mcr.mapToWriteToDisk = mMap;
//增加一個未完成的寫opt
mDiskWritesInFlight++;
//判斷有沒有監聽設定
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
//建立監聽佇列
mcr.keysModified = new ArrayList<String>();
mcr.listeners =
new HashSet<android.content.SharedPreferences.OnSharedPreferenceChangeListener>(mListeners.keySet());
}
//再加一把自己的鎖
synchronized (this) {
//如果調運的是Editor的clear方法,則這裡commit時這麼處理
if (mClear) {
//如果從檔案里加載出來的xml不為空
if (!mMap.isEmpty()) {
//設定資料結構中資料變化標誌為true
mcr.changesMade = true;
//清空記憶體中xml資料
mMap.clear();
}
//處理完畢,標記復位,程式繼續執行,所以如果這次Editor中如果有寫資料且還未commit,則執行完這次commit之後不會清掉本次寫操作的資料,只會clear以前xml檔案中的所有資料
mClear = false;
}
//mModified是調運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;
}
}
//把變化和新加的資料更新到SharePreferenceImpl的mMap中
mMap.put(k, v);
}
//設定資料結構變化標記
mcr.changesMade = true;
if (hasListeners) {
//設定監聽
mcr.keysModified.add(k);
}
}
//清空Editor中零時儲存的資料
mModified.clear();
}
}
//返回重新更新過mMap值封裝的資料結構
return mcr;
}
到此我們Editor的commit方法的第一步已經完成,根據寫操作組織記憶體資料,返回組織後的資料結構。接下來我們繼續回到commit方法看下第二步—-寫到檔案中,其核心是調運SharedPreferencesImpl類的enqueueDiskWrite方法實現。具體如下:
//按照佇列把記憶體資料寫入磁碟,commit時postWriteRunnable為null,apply時不為null
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
//建立一個writeToDiskRunnable的Runnable物件
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
//真正的寫檔案操作
writeToFile(mcr);
}
synchronized (SharedPreferencesImpl.this) {
//寫完一個計數器-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就線上程池中執行
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
可以發現,commit從記憶體寫檔案是在當前調運執行緒中直接執行的。那我們再來看看這個寫記憶體到磁碟方法中真正的寫方法writeToFile,如下:
// Note: must hold mWritingToDiskLock
private void writeToFile(MemoryCommitResult mcr) {
if (mFile.exists()) {
if (!mcr.changesMade) {
//如果檔案存在且沒有改變的資料則直接返回寫OK
mcr.setDiskWriteResult(true);
return;
}
if (!mBackupFile.exists()) {
//如果要寫入的檔案已經存在,並且備份檔案不存在時就先把當前檔案備份一份,因為如果本次寫操作失敗時資料可能已經亂了,所以下次例項化load資料時可以從備份檔案中恢復
if (!mFile.renameTo(mBackupFile)) {
Log.e(TAG, "Couldn't rename file " + mFile
+ " to backup file " + mBackupFile);
//命名失敗直接返回寫失敗了
mcr.setDiskWriteResult(false);
return;
}
} else {
//備份檔案存在就把原始檔刪掉,因為要寫新的
mFile.delete();
}
}
try {
//建立mFile檔案
FileOutputStream str = createFileOutputStream(mFile);
if (str == null) {
//建立失敗直接返回寫失敗了
mcr.setDiskWriteResult(false);
return;
}
//把資料寫入mFile檔案
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
//徹底同步到磁碟檔案中
FileUtils.sync(str);
str.close();
//設定檔案許可權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()) {
//上面如果出錯了就刪掉,因為寫之前已經備份過資料了,下次load時load備份資料
if (!mFile.delete()) {
Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
}
}
//寫失敗了
mcr.setDiskWriteResult(false);
}
回過頭可以發現,上面commit的第二步寫磁碟操作其實是做了類似資料庫的事務操作機制的(備份檔案)。接著可以繼續分析commit方法的第三四步,很明顯可以看出,第三步就是回撥設定的監聽方法,通知資料變化了,第四步就是返回commit寫檔案是否成功。
總體到這裡你可以發現,一個常用的SharePreferences過程已經完全分析完畢。接下來我們就再簡單說說Editor的apply方法原理,先來看下Editor的apply方法,如下:
public void apply() {
//有了上面commit分析,這個雷同,寫資料到記憶體,返回資料結構
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
//等待寫檔案結束
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
QueuedWork.add(awaitCommit);
//一個收尾的Runnable
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.remove(awaitCommit);
}
};
//這個上面commit已經分析過的,這裡postWriteRunnable不為null,所以會在一個新的執行緒池調運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類似,只不過他是非同步寫的,沒在當前執行緒執行寫檔案操作,還有就是他不像commit一樣返回檔案是否寫成功狀態。
3-4 SharePreferences原始碼分析總結
題外趣事: 記得好像是去年有一次我用SharePreferences儲存了幾個boolean值,由於開發除錯,當時我直接進入系統data目錄下應用的xml儲存資料夾,然後執行了刪除操作;接著我沒有重啟應用,直接打斷點除錯,握草!奇蹟的發現SharePreferences調運get時竟然拿到了不是初始化的值。哈哈,其實就是上面分析的,載入完後是一個靜態的map,程序沒掛之前一直用的記憶體資料。
通過上面的例項及原始碼分析可以發現:
SharedPreferences在例項化時首先會從sdcard非同步讀檔案,然後快取在記憶體中;接下來的讀操作都是記憶體快取操作而不是檔案操作。
在SharedPreferences的Editor中如果用commit()方法提交資料,其過程是先把資料更新到記憶體,然後在當前執行緒中寫檔案操作,提交完成返回提交狀態;如果用的是apply()方法提交資料,首先也是寫到記憶體,接著在一個新執行緒中非同步寫檔案,然後沒有返回值。
由於上面分析了,在寫操作commit時有三級鎖操作,所以效率很低,所以當我們一次有多個修改寫操作時等都批量put完了再一次提交確認,這樣可以提高效率。
可以發現,在簡單資料行為狀態儲存中,Android的SharedPreferences是一個安全而且不錯的選擇。
4 SharePreferences進階專案解決方案
其實分析完原始碼之後也就差不多了。這裡所謂的進階專案解決方案只是曲線救國的2B行為,只是表明還有這麼一種方案,至於專案中是否值得提倡那就要綜合酌情考慮了。
4-1 SharePreferences儲存複雜物件的解決案例
這個案例完全有些多餘(因為看完這個例子你會發現還不如使用github上大神流行的ACache更爽呢!),但是也比較有意思,所以還是拿出來說說,其實網上實現的也很多。
我們有時候可能會涉及到儲存一個自定義物件到SharedPreferences中,這個怎麼實現呢?標準的SharedPreferences的Editor只提供幾個常見型別的put方法呀,其實可以實現的,原理就是Base64轉碼為字串儲存,如下給出我的一個工具類,在專案中可以直接使用:
/**
* @author 工匠若水
* @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;
}
}
好了,沒啥解釋的,依據需求自己決定吧,這只是一種方案而已,其替代方案很多。
4-2 SharePreferences跨程序訪問解決案例
Android系統有自己的一套安全機制,當應用程式在安裝時系統會分配給他們一個唯一的userid,當該應用程式需要訪問檔案等資源的時候必須要匹配userid。預設情況下安卓應用程式建立的各種檔案(SharePreferences、資料庫等)都是私有的(在/data/data/[APP PACKAGE NAME]/files
),其他程式無法訪問。在3.0以後必須在建立檔案時顯示的指定了Context.MODE_WORLD_READABLE或者Context.MODE_WORLD_WRITEABLE才能被其他程序(應用)訪問。
這個案例也就類似於Android的ContentProvider,可以跨程序訪問,但是其原理是給許可權然後多程序檔案訪問而已。具體我們看如下一個案例,一個用來類比當服務端,一個用來類比當客戶端,如下:
服務端:
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 AppCompatActiv