寫在前面
昨天在用到SharedPreferences時發現用了這麼長時間的SharedPreferences雖然簡單,但是對原理還一知半解,理解的不夠透徹,故今天花點時間總結一下,讓自己有更深刻的印象,也希望對你有所幫助。
1.SharedPreferences介紹
SharedPreferences是Android系統用於儲存如配置資訊等輕量級資料的介面(注意:這裡是介面哦),實際上儲存資料的是一個xml檔案,這些資料以鍵值對的形式存在,比如下面這樣:
2.SharedPreferences的簡單使用
第一步:建立SharedPreferences物件
SharedPreferences的建立有四種方式,分別為:
Context.MODE_PRIVATE
預設模式,表示SharedPreferences檔案是私有的,只能由建立xml檔案的應用程式訪問(該模式在每次寫資料的時候會把原來的資料覆蓋);Context.MODE_APPEND
追加模式,該模式會檢查需要建立的檔案是否已經建立,如果已經建立了,寫入的資料會追加到檔案的末尾;Context.MODE_WORLD_READABLE
開放讀模式,該模式允許其他應用程式讀當前應用程式的SharedPreferences檔案,存在資料安全的風險;Context.MODE_WORLD_WRITEABLE
開放寫模式,該模式允許其他應用程式往當前應用程式的SharedPreferences檔案中寫資料,同樣存在資料安全的風險。
第二步:獲取Edit物件
第三步:通過Edit物件儲存鍵值對資料
需要呼叫commit()
或者apply()
方法才能把資料成功存入檔案中
第四步:通過SharedPreferences物件獲取資料
SharedPreferences使用示例程式碼:
public class SPUtils {
private static final String SP_NAME = "hello";
private static SPUtils mSPUtils = null;
private SharedPreferences mSharedPreferences = null;
private SharedPreferences.Editor mEditor = null;
private Context mContext = null;//全域性Context
private SPUtils(Context context) {
this.mContext = context;
this.mSharedPreferences = this.mContext.getSharedPreferences(SP_NAME, Context.MODE_APPEND);
this.mEditor = this.mSharedPreferences.edit();
}
public static SPUtils getInstance(Context context) {
if (mSPUtils == null) {
synchronized (SPUtils.class) {
if (mSPUtils == null) {
mSPUtils = new SPUtils(context);
}
}
}
return mSPUtils;
}
//存入Value型別為String的資料
public void putString(String key, String value) {
this.mEditor.putString(key, value);
this.mEditor.commit();
}
//獲取Value型別為String的資料
public String getString(String key, String defValue) {
return this.mSharedPreferences.getString(key, defValue);
}
}複製程式碼
3.SharedPreferences原始碼解析
首先應該從this.mSharedPreferences = this.mContext.getSharedPreferences(SP_NAME, Context.MODE_APPEND);
背後的原理說起。
SharedPreferences物件是Context中getSharedPreferences()方法返回的,我們先找到這個方法,在IDE中經過搜尋,發現Context中有兩種getSharedPreferences()方法,如下:
從圖中我們很明顯的看出來兩個方法的第一個引數型別不同,在實際使用中,一般都是呼叫第一個引數為String型別的方法
getSharedPrefereces(String,int)
,我們進入這個方法看看...
public abstract class Context{
...
public abstract SharedPreferences getSharedPreferences(String name,@PreferencesMode int mode);
...
}
複製程式碼
發現這個方法是一個抽象方法,那麼
getSharedPrefereces(File,int)
是不是也是抽象方法呢,進入看一眼...
public abstract class Context{
...
public abstract SharedPreferences getSharedPreferences(File file,int mode);
...
}
複製程式碼
的確,它也是一個抽象方法,那麼問題就來了,它們的具體實現是在哪裡呢?既然是Context中的方法,我們還得從Context這個抽象類出發,在前面SharedPreferences的使用示例中Context使用的是全域性的Context,也就是Application對應的Context,在分析該Context之前,我們先來了解一下Context的幾個重要的繼承關係:
從圖中可以看出,Context的兩個直接子類是ContextWrapper和ContextImpl,從這兩個類的類名大概能猜出它們的功能:ContextWrapper類主要是對Context的功能封裝,ContextImpl類則是對Context的功能實現,到這裡我們的思路就明朗了,既然ContextImpl是Context的實現類,那麼
getSharedPreferences(File,int)
和getSharedPreferences(String,int)
的實現應該就在ContextImpl類中,具體是不是,我們繼續向下看...Application對應的Context物件我們通常是通過
getApplicationContext()
方法獲取的,我們在Application類中搜尋該方法:從搜尋結果可以看出該方法指向ContextWrapper類,這說明了Application是ContextWrapper的子類,而呼叫Application的
getApplicationContext()
其實呼叫的是ContextWrapper類中的方法,我們進入ContextWrapper中看一看...
public class ContextWrapper extends Context{
Context mBase;
...
protected void attachBaseContext(Context base){
if(mBase != null){
throw new IllegalStateException("Base context already set");
}
mBase = base;
}
...
@Override
public Context getApplicationContext(){
return mBase.getApplicationContext();
}
}
複製程式碼
ContextWrapper類中有一個全域性Context型別的
mBase
變數,它是在attachBaseContext(Context base)
方法中被賦予引用的,其實這裡的引用指向的就是一個ContextImpl物件,在getApplicationContext()
方法中呼叫了mBase
的getApplicationContext()
方法,也就是ContextImpl類中的getApplicationContext()
,我們進入ContextImpl類中的getApplicationContext()
方法...
class ContextImpl extends Context{
...
@Override
public Context getApplicationContext() {
return (mPackageInfo != null) ?
mPackageInfo.getApplication() : mMainThread.getApplication();
}
...
}
複製程式碼
不管是LoadedApk中的
getApplication()
還是ActivityThread中的getApplication()
,最終獲得的都是當前使用的Application物件,也就是說如果我們自定義了一個Application,那麼getApplicationContext()
方法返回的是當前自定義的Application物件,轉了一個大圈,最後我們還是要回到Application物件上,因此,在這行程式碼中this.mSharedPreferences = this.mContext.getSharedPreferences(SP_NAME, Context.MODE_APPEND);
呼叫的getSharedPreferences(String,int)
其實是Application中的,我們在Application類中搜尋該方法:從搜尋結果可以看出該方法是指向ContextWrapper類的,也就是說它是ContextWrapper中的方法,我們進入看一眼...
public class ContextWrapper extends Context{
Context mBase;
...
protected void attachBaseContext(Context base){
if(mBase != null){
throw new IllegalStateException("Base context already set");
}
mBase = base;
}
...
@Override
public Context getApplicationContext(){
return mBase.getApplicationContext();
}
@Override
public SharedPreferences getSharedPreferences(String name,int mode){
return mBase.getSharedPreferences(name,mode);
}
}複製程式碼
在該方法內部又呼叫了mBase
的getSharedPreferences(String,int)
方法,再進入ContextImpl中的getSharedPreferences(String,int)
方法...
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
// 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";
}
}
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();//1
}
file = mSharedPrefsPaths.get(name);
if (file == null) {
file = getSharedPreferencesPath(name);//2
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);//3
}複製程式碼
哎呀,終於開始了,哈哈哈,這個方法中我們主要關注三個地方,分別對應註釋1,2,3,先來看註釋1,mSharedPrefsPaths
是一個全域性的Map,它的宣告如下:
private ArrayMap<String,File> mSharedPrefsPaths;複製程式碼
從getSharedPreferences(String,int)
方法中的上下邏輯可以知道該Map的key值存放的是外面傳進來的SharedPreferences檔案的名稱,也就是SharedPreferences使用示例中的字串"hello",value存放的是File型別的物件,該物件是在註釋2處獲取的,我們進入註釋2的getSharedPreferencesPath(String)
方法...
class ContextImpl extends Context{
...
@Override
public File getSharedPreferencesPath(String name){
return makeFilename(getPreferencesDir(),name + ".xml");
}
...
}複製程式碼
該方法中又呼叫了getPreferencesDir()方法...
class ContextImpl extends Context{
...
@Override
public File getSharedPreferencesPath(String name){
return makeFilename(getPreferencesDir(),name + ".xml");
}
private File getPreferencesDir(){
synchronized(){
if(mPreferencesDir == null){
mPreferencesDir = new File(getDataDir(),"shared_prefs");
}
return ensurePrivateDirExists(mPreferencesDir);
}
}
...
}複製程式碼
從該方法中我們知道這是在建立"shared_prefs"為名的資料夾,完整路徑應該是"/data/data/應用程式包名/shared_prefs/",在該資料夾下會存放名為name的xml子檔案,資料夾建立好了之後,把File物件返回去,而後,繼續呼叫makeFilename()方法,我們來看一眼這個方法...
private File makeFilename(File base, String name) {
if (name.indexOf(File.separatorChar) < 0) {
return new File(base, name);
}
throw new IllegalArgumentException(
"File " + name + " contains a path separator");
}複製程式碼
該方法內部new了一個以"/data/data/應用程式包名/shared_prefs/"為父目錄,name為子檔案的File物件,並把該物件返回,該物件就是註釋2處獲取到的File物件,我們再回到註釋2處繼續向下看...
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
// 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";
}
}
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();//1
}
file = mSharedPrefsPaths.get(name);
if (file == null) {
file = getSharedPreferencesPath(name);//2
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);//3
}複製程式碼
在註釋2處的程式碼走完之後,mSharedPrefsPaths
會把File物件和name暫存在ContextImpl中(這裡體現在ContextWrapper的mBase變數)。我們再來看註釋3,該處呼叫的是ContextImpl類的getSharedPreferences(File,int)
方法,我們進入該方法...
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
checkMode(mode);
if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
if (isCredentialProtectedStorage()
&& !getSystemService(StorageManager.class).isUserKeyUnlocked(
UserHandle.myUserId())
&& !isBuggy()) {
throw new IllegalStateException("SharedPreferences in credential encrypted "
+ "storage are not available until after user is unlocked");
}
}
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();//a
sp = cache.get(file);
if (sp == null) {
sp = new SharedPreferencesImpl(file, mode);//b
cache.put(file, sp);
return sp;//c
}
}
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;
}複製程式碼
該方法中我們只需要關注a,b,c三處的程式碼,首先來看看註釋a,在註釋a處呼叫了getSharedPreferencesCacheLocked()
,該方法返回的是一個以File物件為key,SharedPreferencesImpl物件為value的Map,我們進入getSharedPreferencesCacheLocked()
方法...
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>();
}
final String packageName = getPackageName();
ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}
return packagePrefs;
}複製程式碼
該方法中我們發現有一個sSharedPrefsCache
的全域性靜態變數,它的宣告如下...
class ContextImpl extends Context{
...
priavate static ArrayMap<String,ArrayMap<File,SharedPreferencesImpl>> sSharedPrefsCache;
...
}複製程式碼
再回到剛才的註釋a處繼續向下看,我們發現sSharedPrefsCache
key存放的是應用程式的包名,value存放的是以File物件為key,SharedPreferencesImpl物件為value的map。繼續,在註釋b處new了一個SharedPreferencesImpl物件,並把File和Mode傳進入了,我們進入SharedPreferencesImpl的構造方法...
final class SharedPreferencesImpl implements SharedPreferences{
...
private final File mFile;
private final File mBackupFile;
private final int mMode;
private Map<String, Object> mMap; // guarded by 'this'
private boolean mLoaded = false; // guarded by 'this'
SharedPreferencesImpl(File file, int mode) {
//儲存的檔案
mFile = file;
//建立跟原檔名相同的備份檔案
mBackupFile = makeBackupFile(file);
//訪問模式
mMode = mode;
mLoaded = false;
mMap = null;
//從flash或者sdcard中非同步載入檔案資料
startLoadFromDisk();
}
static File makeBackupFile(File prefsFile) {
//new一個跟原檔名相同的以.bak為字尾的備份檔案
return new File(prefsFile.getPath() + ".bak");
}
private void startLoadFromDisk() {
//對mLoaded標誌位進行同步操作
synchronized (this) {
mLoaded = false;
}
//開啟一個執行緒載入檔案資料
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
private void loadFromDisk() {
//持有SharedPreferencesImpl物件鎖
synchronized (SharedPreferencesImpl.this) {
//如果已經載入過了,直接返回
if (mLoaded) {
return;
}
//如果存在備份檔案則把原檔案刪除,備份檔案按File檔案來命名
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的格式
map = XmlUtils.readMapXml(str);
} catch (XmlPullParserException | IOException e) {
Log.w(TAG, "getSharedPreferences", e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
/* ignore */
}
//加SharedPreferencesImpl物件鎖,防止在getXXX()時出錯
synchronized (SharedPreferencesImpl.this) {
//到這裡說明檔案載入完畢,mLoaded標誌位置true
mLoaded = true;
if (map != null) {
//把map賦值給全域性變數
mMap = map;
//記錄本次讀取檔案的時間戳
mStatTimestamp = stat.st_mtime;
//記錄檔案的大小
mStatSize = stat.st_size;
} else {
//如果檔案第一次建立沒有資料,則直接new一個HashMap返回供後續putXXX()資料時存放資料
mMap = new HashMap<>();
}
//喚醒其他等待的執行緒,這裡指的是呼叫getXXX()的執行緒,因為在mLoaded為falses時,呼叫getXXX()的執行緒會進入wait()狀態
notifyAll();
}
}
...
}複製程式碼
上面我已經把關鍵的程式碼和註釋貼出來了,這裡不再細說...
小結:
- SharedPreferences物件在第一次例項化的時候會從xml檔案中讀取資料並儲存在Map中(也就是記憶體中);
- 在以後的使用中,會先根據xml檔名在ContextImpl的mSharedPrefsPaths中找到對應的File物件,然後根據包名在靜態全域性變數sSharedPrefsCache中找出對應的File-SharedPreferencesImpl map集合,再根據前面獲得的File物件得到SharedPreferencesImpl物件。
- 從第二條總結來看,我們知道SharedPreferences一旦建立了之後會一直存在於系統中,需要使用時直接就能拿到。
至此,整個SharedPreferences建立過程就解析完了,下面我們來看看資料是如何獲取和儲存的...
以getString(String,String)
為例:
final class SharedPreferencesImpl implements SharedPreferences{
...
private final File mFile;
private final File mBackupFile;
private final int mMode;
private Map<String, Object> mMap; // guarded by 'this'
private boolean mLoaded = false; // guarded by 'this'
SharedPreferencesImpl(File file, int mode) {
//儲存的檔案
mFile = file;
//建立跟原檔名相同的備份檔案
mBackupFile = makeBackupFile(file);
//訪問模式
mMode = mode;
mLoaded = false;
mMap = null;
//從flash或者sdcard中非同步載入檔案資料
startLoadFromDisk();
}
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (this) {
//如果xml檔案沒有載入解析完畢,呼叫執行緒一直wait
awaitLoadedLocked();
//從記憶體中獲取key所對應的value值
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();
}
//mLoaded為false一直wait
while (!mLoaded) {
try {
wait();
} catch (InterruptedException unused) {
}
}
}
...
}複製程式碼
上面只貼出了getString()的程式碼,其他幾種原理差不多,這裡不再一一解釋,到這兒,資料的獲取就講完了。下面我們再來看看putXXX()方法,儲存資料,由於putXXX()方法是依靠Editor物件來操作的,我們先來看看建立Editor物件的過程...
final class SharedPreferencesImpl implements SharedPreferences{
...
private final File mFile;
private final File mBackupFile;
private final int mMode;
private Map<String, Object> mMap; // guarded by 'this'
private boolean mLoaded = false; // guarded by 'this'
SharedPreferencesImpl(File file, int mode) {
//儲存的檔案
mFile = file;
//建立跟原檔名相同的備份檔案
mBackupFile = makeBackupFile(file);
//訪問模式
mMode = mode;
mLoaded = false;
mMap = null;
//從flash或者sdcard中非同步載入檔案資料
startLoadFromDisk();
}
public Editor edit() {
// TODO: remove the need to call awaitLoadedLocked() when
// requesting an editor. will require some work on the
// Editor, but then we should be able to do:
//
// context.getSharedPreferences(..).edit().putString(..).apply()
//
// ... all without blocking.
//這裡和非同步的mLoaded使用的是同一把鎖,因為這裡面也用到了mLoaded標誌位
synchronized (this) {
//mLoaded為false時等待
awaitLoadedLocked();
}
//new一個EditorImpl物件返回
return new EditorImpl();
}
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();
}
//mLoaded為false一直wait
while (!mLoaded) {
try {
wait();
} catch (InterruptedException unused) {
}
}
}
...
}複製程式碼
在edit()
方法中最後new了一個EditorImpl物件,進入EditorImpl類...
public final class EditorImpl implements Editor {
//建立一個key-value的集合,用來暫存putXXX()資料
private final Map<String, Object> mModified = Maps.newHashMap();
//是否清除SharedPreferences的標誌位
private boolean mClear = false;
public Editor putString(String key, @Nullable String value) {
//同步鎖
synchronized (this) {
//將要儲存的資料暫存在mModified中
mModified.put(key, value);
//返回當前物件,方便鏈式呼叫
return this;
}
}
public Editor remove(String key) {
//同步刪除key-value
synchronized (this) {
//這裡為什麼是put呢,而不是remove,原因在後面會講解
mModified.put(key, this);
return this;
}
}
public Editor clear() {
//清空所有資料只需要把mClear標誌位置為true
synchronized (this) {
mClear = true;
return this;
}
}
...
}複製程式碼
從上面的程式碼中我們發現put的資料只是暫存到了mModified
變數中,並沒有我們想象中那樣直接儲存到檔案,這樣就對了,因為我們在儲存資料時最後還要呼叫commit()或者apply()方法呢,因此我們就可以大膽的猜測資料寫入檔案的操作是在commit()或者apply()方法中進行的,好了,廢話不多說,我們先來分析commit()方法...
public final class EditorImpl implements Editor {
//建立一個key-value的集合,用來暫存putXXX()資料
private final Map<String, Object> mModified = Maps.newHashMap();
//是否清除SharedPreferences的標誌位
private boolean mClear = false;
//先來看看commit
public boolean commit() {
//把資料儲存到mMap中,並在MemoryCommitResult中持有mMap的引用
MemoryCommitResult mcr = commitToMemory();
//把mMap中的資料存入到xml檔案
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;
}
// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
//new了一個MemoryCommitResult物件,MemoryCommitResult是SharedPreferences中的靜態內部類
MemoryCommitResult mcr = new MemoryCommitResult();
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);
}
//把mMap賦值到MemoryCommitResult中的要寫到disk的Map
mcr.mapToWriteToDisk = mMap;
//增加一個未完成的寫操作
mDiskWritesInFlight++;
//判斷有沒有監聽
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
mcr.keysModified = new ArrayList<String>();
mcr.listeners =
new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
synchronized (this) {
//如果呼叫了clear()方法,該mClear標誌位就是true
if (mClear) {
if (!mMap.isEmpty()) {
mcr.changesMade = true;
//清空記憶體中mMap,並不會清空快取在Editor中的資料
mMap.clear();
}
//mClear重置為false
mClear = false;
}
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.
//在remove()方法中put(key,this),用於刪除資料
if (v == this || v == null) {
//如果mMap中包含這個key,就會把這個key對應的key-value刪除
if (!mMap.containsKey(k)) {
continue;
}
mMap.remove(k);
} else {
if (mMap.containsKey(k)) {
Object existingValue = mMap.get(k);
//如果原來mMap中有對應的key-value值不會重複新增
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mMap.put(k, v);
}
mcr.changesMade = true;
//如果有監聽器,把變化的key放入MemoryCommitResult中的List
if (hasListeners) {
mcr.keysModified.add(k);
}
}
//資料向mMap中存完之後清空Editor中的mModified
mModified.clear();
}
}
//返回MemoryCommitResult物件
return mcr;
}
...
}
// Return value from EditorImpl#commitToMemory()
private static class MemoryCommitResult {
public boolean changesMade; // any keys different?
public List<String> keysModified; // may be null
public Set<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();
}
}複製程式碼
上面我們只分析了commit()方法中的commitToMemory()
方法,該方法主要是把Eidtor中快取的資料存入mMap中(也就是我們俗稱的“記憶體”中),並且MemoryCommitResult中的mapToWriteToDisk持有該map的引用,接下來我們分析enqueueDiskWrite()方法...
public final class EditorImpl implements Editor {
//建立一個key-value的集合,用來暫存putXXX()資料
private final Map<String, Object> mModified = Maps.newHashMap();
//是否清除SharedPreferences的標誌位
private boolean mClear = false;
//先來看看commit
public boolean commit() {
//把資料儲存到mMap中,並在MemoryCommitResult中持有mMap的引用
MemoryCommitResult mcr = commitToMemory();
//把mMap中的資料存入到xml檔案
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
//如果有監聽器並且資料有改變則通知這些監聽器
notifyListeners(mcr);
//寫成功返回true,寫失敗返回false
return mcr.writeToDiskResult;
}
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);
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
//commit()會走這裡,因為commit是同步方法
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (SharedPreferencesImpl.this) {
//如果只有一個寫操作
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
//一個寫操作直接在當前執行緒中執行寫檔案操作,不用另起執行緒,寫完返回
writeToDiskRunnable.run();
return;
}
}
//如果是apply()會線上程池中執行寫檔案操作
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
// Note: must hold mWritingToDiskLock
//真正的寫檔案操作
private void writeToFile(MemoryCommitResult mcr) {
// Rename the current file so it may be used as a backup during the next read
if (mFile.exists()) {
if (!mcr.changesMade) {
// If the file already exists, but no changes were
// made to the underlying map, it's wasteful to
// re-write the file. Return as if we wrote it
// out.
//如果檔案存在並且沒有改變則直接返回並標記為寫成功
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();
}
}
// Attempt to write the file, delete the backup and return true as atomically as
// possible. If any exception occurs, delete the new file; next time we will restore
// from the backup.
try {
FileOutputStream str = createFileOutputStream(mFile);
if (str == null) {
//檔案輸出流建立失敗直接返回,並標記寫檔案失敗
mcr.setDiskWriteResult(false);
return;
}
//把mMap中的資料寫入mFile檔案中
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
//同步到磁碟檔案中
FileUtils.sync(str);
str.close();
//設定檔案訪問許可權
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
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.
//寫入成功則把備份檔案刪除
mBackupFile.delete();
//設定標誌位為true,表示寫入成功
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()) {
if (!mFile.delete()) {
Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
}
}
//上面如果沒有走成功則說明寫失敗了,置標誌位為false
mcr.setDiskWriteResult(false);
}
...
}複製程式碼
看完上面一坨程式碼和註釋之後,我們發現原來真正寫檔案操作是在這裡。至此,commit()方法分析完了。
小結:
- commit()方法是先把Editor中快取的資料寫進“記憶體”中,並讓MemoryCommitResult的mapToWriteToDisk持有mMap的引用,然後再對mMap進行寫檔案操作,實際引用的是mapToWriteToDisk,再然後就是通知監聽資料變化的例項。
好了,我們再來看看apply()方法...
public void apply() {
//和commit()一樣的操作,先把Editor中快取的資料提交到mMap(記憶體)中
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);
}
};
//這裡的postWriteRunnable不為null,所以會在另一個執行緒中進行寫檔案操作
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()差不多嘛,關鍵的還是這句SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
postWriteRunnable不為空,進入enqueueDiskWrite()方法後就會走執行緒池另起執行緒執行寫檔案操作,因而,commit和apply的區別就在於同步和非同步。
總結
- SharedPreferences物件在第一次例項化的時候會從xml檔案中讀取資料並儲存在Map中(也就是記憶體中);
- 在以後的使用中,會先根據xml檔名在ContextImpl的mSharedPrefsPaths中找到對應的File物件,然後根據包名在靜態全域性變數sSharedPrefsCache中找出對應的File-SharedPreferencesImpl map集合,再根據前面獲得的File物件得到SharedPreferencesImpl物件,不會重複建立;
- getXXX()操作是從mMap中(記憶體中)拿資料;
- putXXX()只是把資料快取在Editor的mModified中,clear()操作也只是改變了一個標誌位,真正把資料提交到記憶體中(mMap)和寫檔案的是commit或者apply;
- commit()有三級鎖,分別為SharedPreferences物件鎖,Editor物件鎖,Object物件鎖,因此效率相對來說比較低,我們在使用時應該集中put操作,最後commit。