SharedPreferences的使用及原始碼淺析

一笨正經的小屁孩發表於2017-11-01

寫在前面

昨天在用到SharedPreferences時發現用了這麼長時間的SharedPreferences雖然簡單,但是對原理還一知半解,理解的不夠透徹,故今天花點時間總結一下,讓自己有更深刻的印象,也希望對你有所幫助。

1.SharedPreferences介紹

SharedPreferences是Android系統用於儲存如配置資訊等輕量級資料的介面(注意:這裡是介面哦),實際上儲存資料的是一個xml檔案,這些資料以鍵值對的形式存在,比如下面這樣:


SharedPreferences的使用及原始碼淺析

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()方法,如下:


SharedPreferences的使用及原始碼淺析

從圖中我們很明顯的看出來兩個方法的第一個引數型別不同,在實際使用中,一般都是呼叫第一個引數為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的幾個重要的繼承關係:

SharedPreferences的使用及原始碼淺析

從圖中可以看出,Context的兩個直接子類是ContextWrapper和ContextImpl,從這兩個類的類名大概能猜出它們的功能:ContextWrapper類主要是對Context的功能封裝,ContextImpl類則是對Context的功能實現,到這裡我們的思路就明朗了,既然ContextImpl是Context的實現類,那麼getSharedPreferences(File,int)getSharedPreferences(String,int)的實現應該就在ContextImpl類中,具體是不是,我們繼續向下看...

Application對應的Context物件我們通常是通過getApplicationContext()方法獲取的,我們在Application類中搜尋該方法:

SharedPreferences的使用及原始碼淺析

從搜尋結果可以看出該方法指向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()方法中呼叫了mBasegetApplicationContext()方法,也就是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類中搜尋該方法:

SharedPreferences的使用及原始碼淺析

從搜尋結果可以看出該方法是指向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);
    }
}複製程式碼

在該方法內部又呼叫了mBasegetSharedPreferences(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處繼續向下看,我們發現sSharedPrefsCachekey存放的是應用程式的包名,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。

相關文章