SharedPreferences原理淺析

SharedPreferences原理淺析

1.概述

SharedPreferences是用來訪問和修改偏好preference數(shù)據(jù)的接口,可以通過Context.getSharedPreferences()方法返回SharedPreferences。

對(duì)于任意一組偏好設(shè)置數(shù)據(jù),只有一個(gè)共享的SharedPreferences實(shí)例。

修改preferences必須通過一個(gè)Editor對(duì)象來確保存儲(chǔ)數(shù)據(jù)的一致性以及控制數(shù)據(jù)何時(shí)存儲(chǔ)。

SharedPreferences是一個(gè)接口,里面定義了很多數(shù)據(jù)存儲(chǔ)與獲取的接口。

public interface SharedPreferences {
    /*
    * 定義了一個(gè)回調(diào)接口,當(dāng)SharedPreference被修改時(shí),觸發(fā)該方法
    */
    public interface OnSharedPreferenceChangeListener {
         void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);
    }

    /*
    * 用來修改SharedPreference里面values的接口
    * 所有在editor中的修改都是批量修改,只有調(diào)用了commit()或者apply()方法之后,修改才生效
    */
    public interface Editor {
        Editor putString(String key, @Nullable String value);
        Editor putInt(String key, int value);
        Editor remove(String key);
        Editor clear();
        boolean commit();
        void apply();
    }

    String getString(String key, @Nullable String defValue);
    int getInt(String key, int defValue);
    Editor edit();
    void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
    void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
}

我們可以通過Context.getSharedPreferences()方法返回一個(gè)SharedPreferences實(shí)現(xiàn),該實(shí)現(xiàn)是SharedPreferencesImpl類。接下來將來分析SharedPreferencesImpl實(shí)現(xiàn)類。

2.源碼分析

2.1 ContextImpl.getSharedPreferences()

public SharedPreferences getSharedPreferences(String name, int mode) {
    // 在Android 4.4以前,如果name為null的話,則會(huì)把它當(dāng)成null.xml來處理
    if (mPackageInfo.getApplicationInfo().targetSdkVersion <
            Build.VERSION_CODES.KITKAT) {
        if (name == null) {
            name = "null";
        }
    }

    File file;
    synchronized (ContextImpl.class) {
        if (mSharedPrefsPaths == null) {
            mSharedPrefsPaths = new ArrayMap<>();
        }
        // 根據(jù)name查找對(duì)應(yīng)的File文件是否存在,不存在,則根據(jù)name創(chuàng)建一個(gè)File文件,并把該File文件保存到一個(gè)Map集合中,以備后續(xù)使用
        file = mSharedPrefsPaths.get(name);
        if (file == null) {
            file = getSharedPreferencesPath(name);
            mSharedPrefsPaths.put(name, file);
        }
    }
    return getSharedPreferences(file, mode);//見2.2
}

在ContextImpl中定義了兩個(gè)與SharedPreference相關(guān)的ArrayMap,它們分別緩存<preference name,File>和<package name,<File,SharedPreferencesImpl>>。它們的定義如下:

 /**
 * Map from package name, to preference name, to cached preferences.
 */
@GuardedBy("ContextImpl.class")
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

/**
 * Map from preference name to generated path.
 */
@GuardedBy("ContextImpl.class")
private ArrayMap<String, File> mSharedPrefsPaths;

我們?cè)诓檎乙粋€(gè)SharedPreferences時(shí),首先需要根據(jù)SharedPreferences的name在mSharedPrefsPaths中查找到對(duì)應(yīng)的File文件,然后根據(jù)當(dāng)前應(yīng)用包名package name,在sSharedPrefsCache中查找當(dāng)前應(yīng)用包名對(duì)應(yīng)的ArrayMap<File, SharedPreferencesImpl>,最后根據(jù)File文件,查找對(duì)應(yīng)的SharedPreferencesImpl類。查找過程大致如下:

preference name ——> File

package name ——> ArrayMap<File,SharedPreferencesImpl>———> SharedPreferencesImpl

2.2 ContextImpl.getSharedPreferences()

public SharedPreferences getSharedPreferences(File file, int mode) {
    checkMode(mode);// 檢查文件模式
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();//根據(jù)當(dāng)前應(yīng)用包名,獲取對(duì)應(yīng)的ArrayMap<File, SharedPreferencesImpl>
        sp = cache.get(file);//根據(jù)文件,獲取對(duì)應(yīng)的SharedPreferencesImpl
        if (sp == null) {
            sp = new SharedPreferencesImpl(file, mode);//創(chuàng)建一個(gè)新的ShardPreferenceImpl對(duì)象,見2.3
            cache.put(file, sp);//存入緩存中
            return sp;
        }
    }
    // 如果是多進(jìn)程模式,或者是Android 3.0以前,則檢測(cè)是否有其他進(jìn)程在后臺(tái)修改SharedPreferences
    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;
}

SharedPreference的創(chuàng)建模式mode有以下幾種:

  • MODE_PRIVATE:默認(rèn)模式,該模式下創(chuàng)建的文件只能被當(dāng)前應(yīng)用或者與該應(yīng)用具有相同SharedUserID的應(yīng)用訪問。
  • MODE_WORLD_READABLE:允許其他應(yīng)用讀取這個(gè)模式創(chuàng)建的文件。在Android N上使用該模式將拋出SecurityException異常。
  • MODE_WORLD_WRITEABLE:允許其他應(yīng)用寫入這個(gè)模式創(chuàng)建的文件。在Android N上使用該模式將拋出SecurityException異常。
  • MODE_APPEND:如果文件已經(jīng)存在了,則在文件的尾部添加數(shù)據(jù)。
  • MODE_MULTI_PROCESS:SharedPreference加載標(biāo)志,當(dāng)設(shè)置了該標(biāo)志,則在磁盤上的文件將會(huì)被檢查是否修改了,盡管SharedPreference實(shí)例已經(jīng)在該進(jìn)程中被加載了。

在checkMode()方法中,主要是檢查是否在Android N上使用了MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE模式,如果使用了,則拋出異常。

在獲取SharedPreference時(shí),不是每次都會(huì)重新創(chuàng)建一個(gè)新的SharedPreference實(shí)例,而是先從緩存中,查找是否存在對(duì)應(yīng)的SharedPreference實(shí)例,如果有相應(yīng)的實(shí)例,則直接返回。如果不存在,則創(chuàng)建一個(gè)新的SharedPreference實(shí)例,并把它保存在緩存中,以備下次使用。

private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
    if (sSharedPrefsCache == null) {
        sSharedPrefsCache = new ArrayMap<>();
    }

    final String packageName = getPackageName();//獲取當(dāng)前應(yīng)用的包名
    ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);//獲取當(dāng)前應(yīng)用包名對(duì)應(yīng)的ArrayMap<File, SharedPreferencesImpl>
    if (packagePrefs == null) {
        packagePrefs = new ArrayMap<>();
        sSharedPrefsCache.put(packageName, packagePrefs);//保存到緩存中
    }

    return packagePrefs;
}

由于ContextImpl是應(yīng)用的執(zhí)行環(huán)境,每一個(gè)應(yīng)用里面可以包含有多個(gè)SharedPreference文件。因此,為了更好的定位SharedPreference文件,首先根據(jù)應(yīng)用包名進(jìn)行篩選,得到ArrayMap<File, SharedPreferencesImpl>,然后再通過SharedPreference文件名進(jìn)行篩選,得到SharedPreferencesImpl。

可以看到,SharedPreferencesImpl只會(huì)被創(chuàng)建一次,之后會(huì)被保存在緩存中,后續(xù)的獲取操作都是從緩存中獲取SharedPreferencesImpl實(shí)例對(duì)象。

2.3 SharedPreferencesImpl()

SharedPreferencesImpl(File file, int mode) {
    mFile = file;//保存SharedPreference對(duì)應(yīng)的xml文件
    mBackupFile = makeBackupFile(file);//構(gòu)建備份文件,以.bak后綴結(jié)尾
    mMode = mode;//保存創(chuàng)建模式
    mLoaded = false;//初始化mLoaded為false
    mMap = null;//初始化Map<String, Object>為空
    startLoadFromDisk();//開始從磁盤中加載數(shù)據(jù),見2.4
}

在SharedPreferencesImpl的構(gòu)造函數(shù)中,主要是初始化了一些重要成員變量,并開始從磁盤中加載數(shù)據(jù)到內(nèi)存中。

2.4 SharedPreferencesImpl.startLoadFromDisk()

private void startLoadFromDisk() {
    synchronized (this) {
        mLoaded = false;
    }
    //創(chuàng)建一個(gè)線程來執(zhí)行從磁盤中加載數(shù)據(jù)
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();// 見2.5
        }
    }.start();
}

該方法主要是創(chuàng)建一個(gè)子線程,然后在子線程中執(zhí)行具體的加載操作。

2.5 SharedPreferencesImpl.loadFromDisk()

private void loadFromDisk() {
    synchronized (SharedPreferencesImpl.this) {
        // 如果已經(jīng)被加載了,則直接返回
        if (mLoaded) {
            return;
        }
        //如果備份文件已經(jīng)存在了,則刪除當(dāng)前文件,用備份文件來代替
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }

    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 = XmlUtils.readMapXml(str);//讀取文件內(nèi)容到Map集合中,Map集合是一個(gè)Map<String, Object>的集合
            } catch (XmlPullParserException | IOException e) {
                Log.w(TAG, "getSharedPreferences", e);
            } finally {
                IoUtils.closeQuietly(str);
            }
        }
    } catch (ErrnoException e) {
        /* ignore */
    }

    synchronized (SharedPreferencesImpl.this) {
        mLoaded = true;//將mLoaded置為空,讓等待加載完成的線程被喚醒
        if (map != null) {
            mMap = map;//保存加載內(nèi)容到mMap中
            mStatTimestamp = stat.st_mtime;//記錄時(shí)間戳
            mStatSize = stat.st_size;//記錄文件大小
        } else {
            mMap = new HashMap<>();
        }
        notifyAll();//喚醒被阻塞的線程
    }
}

可以看到,在loadFromDisk()方法中,主要的工作是將磁盤中的文件內(nèi)容讀取到內(nèi)存中,然后再喚醒阻塞等待的線程,告訴他們數(shù)據(jù)已經(jīng)讀取到內(nèi)存中了。這里用到了典型的wait()/notifyAll()機(jī)制,來同步線程之間的交互。那在什么時(shí)候會(huì)調(diào)用wait()呢?在我們獲取SharedPreferencesImpl來存儲(chǔ)數(shù)據(jù)和獲取數(shù)據(jù)時(shí),都會(huì)調(diào)用到wait()。

SharedPreferences數(shù)據(jù)的存儲(chǔ)需要借助Editor來實(shí)現(xiàn),通過Editor操作后,再通過commit()或apply()方法將修改同步到磁盤中。commit()方法是有返回結(jié)果的,來表示修改是否成功了。而apply()方法是沒有返回結(jié)果,它只是提交一個(gè)寫入磁盤的請(qǐng)求,然后由子線程去執(zhí)行。

Editor是通過SharedPreferencesImpl的edit()方法來創(chuàng)建的。

2.6 SharedPreferencesImpl.edit()

public Editor edit() {
    synchronized (this) {//同步方法,保證每次只有一個(gè)線程執(zhí)行加載操作
        awaitLoadedLocked();//等待SharedPreferencesImpl加載到內(nèi)存中,見2.7
    }

    return new EditorImpl();//創(chuàng)建一個(gè)新的Editor實(shí)現(xiàn)。
}

在創(chuàng)建Editor之前,需要等待SharedPreferencesImpl加載到內(nèi)存中,然后才會(huì)創(chuàng)建一個(gè)Editor實(shí)現(xiàn)類EditorImpl。

2.7 SharedPreferencesImpl.awaitLoadedLocked()

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();
    }
    // 如果mLoad為false,則一直循環(huán)等待下去
    while (!mLoaded) {
        try {
            wait();//阻塞等待
        } catch (InterruptedException unused) {
        }
    }
}

在awaitLoadedLocked()方法中,主要是通過mLoaded變量來控制循環(huán)阻塞等待,該變量是在SharedPreferencesImpl的loadFromDisk()方法中,被置為true了,并通過notifyAll()方法喚醒等待的線程。

因?yàn)閺拇疟P中加載SharedPreference數(shù)據(jù)到內(nèi)存中是一個(gè)耗時(shí)操作,因此需要在子線程中執(zhí)行加載操作,當(dāng)子線程加載完成后,需要給主線程發(fā)送一個(gè)通知,喚醒被阻塞等待的操作。

在開始創(chuàng)建SharedPreferencesImpl時(shí),就會(huì)從磁盤中加載xml文件到內(nèi)存中,加載完成后,將mLoaded置為true,并喚醒正在等待的線程。因?yàn)樵谕ㄟ^Context.getSharedPreferences()獲取到SharedPreferencesImpl時(shí),此時(shí)有可能數(shù)據(jù)并未全部都從磁盤加載到內(nèi)存中,因此需要在操作SharedPreferencesImpl之前,等待數(shù)據(jù)從磁盤加載到內(nèi)存中。awaitLoadedLocked()操作就是用來完成等待數(shù)據(jù)從磁盤加載到內(nèi)存中,該方法返回后,可以確保所有的數(shù)據(jù)都加載到內(nèi)存中了,后續(xù)的所有操作都是針對(duì)內(nèi)存中數(shù)據(jù)進(jìn)行操作了。

EditorImpl實(shí)現(xiàn)了Editor接口,獲取到EditorImpl之后,就可以通過Editor對(duì)SharedPreference中的數(shù)據(jù)進(jìn)行修改了。Editor的putXXX方法對(duì)數(shù)據(jù)的修改都只是在內(nèi)存中對(duì)數(shù)據(jù)進(jìn)行修改,只有調(diào)用了commit()或apply()方法之后,才會(huì)真正同步修改到磁盤中。

2.8 EditorImpl.putString()

public Editor putString(String key, @Nullable String value) {
    synchronized (this) {
        mModified.put(key, value);
        return this;
    }
}

EditorImpl的putXXX方法,主要是將數(shù)據(jù)保存在一個(gè)Map中,這些數(shù)據(jù)是存儲(chǔ)在內(nèi)存中,只有調(diào)用了commit()或apply()方法之后,才會(huì)同步到磁盤中。

private final Map<String, Object> mModified = Maps.newHashMap();//暫時(shí)保存需要寫入磁盤的數(shù)據(jù)

2.9 EditorImpl.commit()

public boolean commit() {
    MemoryCommitResult mcr = commitToMemory();//構(gòu)建需要寫入磁盤的數(shù)據(jù),見2.10
    SharedPreferencesImpl.this.enqueueDiskWrite(
        mcr, null /* sync write on this thread okay*/);//見2.11
    try {
        mcr.writtenToDiskLatch.await();//等待寫入完成
    } catch (InterruptedException e) {
        return false;
    }
    notifyListeners(mcr);//通知等待者,寫入已經(jīng)完成了
    return mcr.writeToDiskResult;//返回寫入是否成功
}

在將內(nèi)存中保存的數(shù)據(jù)寫入到磁盤時(shí),需要借助MemoryCommitResult類,構(gòu)建寫入磁盤的數(shù)據(jù)。然后將這個(gè)寫入操作由子線程來執(zhí)行,并等待子線執(zhí)行完成。當(dāng)子線程寫入完成后或者發(fā)生了異常,通知等待者寫入完成了,并把寫入結(jié)果返回給調(diào)用者。

2.10 EditorImpl.commitToMemory()

private MemoryCommitResult commitToMemory() {
    MemoryCommitResult mcr = new MemoryCommitResult();//創(chuàng)建MemoryCommitResult實(shí)例
    synchronized (SharedPreferencesImpl.this) {
        // 在準(zhǔn)備將內(nèi)存中的數(shù)據(jù)寫入到磁盤時(shí),如果已經(jīng)正在執(zhí)行寫入操作,則先采用深拷貝,復(fù)制mMap中的數(shù)據(jù)
        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中的數(shù)據(jù)
        }
        mcr.mapToWriteToDisk = mMap;//將該Map賦值給需要寫入磁盤的Map。
        mDiskWritesInFlight++;//更新正在執(zhí)行寫入磁盤的操作次數(shù)

        boolean hasListeners = mListeners.size() > 0;//返回已經(jīng)注冊(cè)的OnSharedPreferenceChangeListener數(shù)量
        if (hasListeners) {
            mcr.keysModified = new ArrayList<String>();
            mcr.listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
        }

        synchronized (this) {
            boolean changesMade = false;
            //是否需要清除內(nèi)容
            if (mClear) {
                if (!mMap.isEmpty()) {
                    changesMade = true;
                    mMap.clear();
                }
                mClear = false;
            }

            // 循環(huán)將mModified集合中的數(shù)據(jù)拷貝到mMap中
            for (Map.Entry<String, Object> e : mModified.entrySet()) {
                String k = e.getKey();
                Object v = e.getValue();
                // 如果需要修改的value為null,則移除該key對(duì)應(yīng)的entry
                if (v == this || v == null) {
                    if (!mMap.containsKey(k)) {
                        continue;
                    }
                    mMap.remove(k);
                } else {
                    // 如果已經(jīng)存在相同的值,則直接返回,否則將該<k,v>添加到mMap集合中
                    if (mMap.containsKey(k)) {
                        Object existingValue = mMap.get(k);
                        if (existingValue != null && existingValue.equals(v)) {
                            continue;
                        }
                    }
                    mMap.put(k, v);//添加該<k,v>集合到Map中
                }

                changesMade = true;//發(fā)生了改變
                if (hasListeners) {
                    mcr.keysModified.add(k);//更新被修改的key集合
                }
            }

            mModified.clear();//清空mModified Map集合

            if (changesMade) {
                mCurrentMemoryStateGeneration++;
            }

            mcr.memoryStateGeneration = mCurrentMemoryStateGeneration;
        }
    }
    return mcr;
}

可以看到commitToMemory()方法主要完成以下幾件事情:

  1. 創(chuàng)建一個(gè)MemoryCommitResult對(duì)象,該對(duì)象封裝了一些寫入磁盤的狀態(tài);
  2. 對(duì)mMap集合做一個(gè)深拷貝,并把它保存在MemoryCommitResult的mapToWriteToDisk變量中;
  3. 如果注冊(cè)了OnSharedPreferenceChangeListener監(jiān)聽者,則創(chuàng)建一個(gè)ArrayList列表,來保存被修改的key列表;
  4. 如果設(shè)置了清除標(biāo)志位mClear,則先清空mMap集合;
  5. 將EditorImpl中mModified集合中的數(shù)據(jù)拷貝到mMap集合中,如果key對(duì)應(yīng)value已經(jīng)存在了,則跳過拷貝。如果key對(duì)應(yīng)的value為null,則刪除該key對(duì)應(yīng)的Entry。
  6. 清空mModified集合,并返回創(chuàng)建的MemoryCommitResult對(duì)象。

將mModified集合中的數(shù)據(jù)拷貝到mMap集合中,具有緩存的作用,如果應(yīng)用再次馬上查詢SharedPreference時(shí),則可以先從mMap集合中直接返回結(jié)果,而不用從磁盤中讀取。

2.11 EditorImpl.enqueueDiskWrite()

private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
    final boolean isFromSyncCommit = (postWriteRunnable == null);//是否同步提交

    final Runnable writeToDiskRunnable = new Runnable() {
            public void run() {
                synchronized (mWritingToDiskLock) {
                    writeToFile(mcr, isFromSyncCommit);//寫入到磁盤文件中,見2.12
                }
                synchronized (SharedPreferencesImpl.this) {
                    mDiskWritesInFlight--;//寫入完成后,更新正在執(zhí)行寫入操作數(shù)
                }
                if (postWriteRunnable != null) {
                    postWriteRunnable.run();
                }
            }
        };

    if (isFromSyncCommit) {//如果是同步寫入操作
        boolean wasEmpty = false;
        synchronized (SharedPreferencesImpl.this) {
            wasEmpty = mDiskWritesInFlight == 1;//在創(chuàng)建MemoryCommitResult時(shí),如果之前的寫入操作都完成了的話,則mDiskWritesInFlight的值為1
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();//執(zhí)行run()方法,同步調(diào)用
            return;
        }
    }

    QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);//如果之前還有寫入操作,則將該寫入操作放入工作隊(duì)列中,等待執(zhí)行。
}

在enqueueDiskWrite()方法中,首先根據(jù)參數(shù)postWriteRunnable是否為null來判斷是否同步寫入操作。接著創(chuàng)建一個(gè)寫入磁盤的Runnable,在該Runnable中執(zhí)行具體的寫入磁盤文件的操作。如果是同步寫入操作,并且當(dāng)前沒有寫入操作,則直接調(diào)用writeToDiskRunnable的run()方法,在當(dāng)前線程中執(zhí)行磁盤寫入操作。如果是同步寫入操作,并且當(dāng)前有正在執(zhí)行的寫入操作,則將該writeToDiskRunnable放入工作隊(duì)列中,等待線程隨后執(zhí)行。

QueuedWork.singleThreadExecutor()方法返回的是一個(gè)單個(gè)線程的執(zhí)行器Executor,里面有一個(gè)無界的隊(duì)列來保存執(zhí)行任務(wù)。這樣的話,可以保證任務(wù)是順序的執(zhí)行,并且保證每次只有一個(gè)任務(wù)執(zhí)行。

public static ExecutorService singleThreadExecutor() {
    synchronized (QueuedWork.class) {
        if (sSingleThreadExecutor == null) {
            // TODO: can we give this single thread a thread name?
            sSingleThreadExecutor = Executors.newSingleThreadExecutor();
        }
        return sSingleThreadExecutor;
    }
}

2.12 SharedPreferencesImpl.writeToFile()

 private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
    
    if (mFile.exists()) {// 如果文件已經(jīng)存在了
        boolean needsWrite = false;

        // 只有磁盤上文件狀態(tài)比當(dāng)前文件更舊時(shí),才執(zhí)行更新操作
        if (mDiskStateGeneration < mcr.memoryStateGeneration) {
            if (isFromSyncCommit) {//同步寫入操作
                needsWrite = true;
            } else {
                synchronized (this) {
                    // No need to persist intermediate states. Just wait for the latest state to
                    // be persisted.
                    if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                        needsWrite = true;
                    }
                }
            }
        }
        
        // 不需要立即寫入,則在MemoryCommitResult中記錄該結(jié)果,然后直接返回
        if (!needsWrite) {
            mcr.setDiskWriteResult(true);//記錄寫入成功了,喚醒等待寫入結(jié)果的線程
            return;
        }

        if (!mBackupFile.exists()) {//如果備份文件不存在
            if (!mFile.renameTo(mBackupFile)) {//將新文件重命名為備份文件
                mcr.setDiskWriteResult(false);
                return;
            }
        } else {
            mFile.delete();//如果備份文件已經(jīng)存在了,則刪除mFile文件
        }
    }

    // 當(dāng)嘗試寫入文件時(shí),刪除備份文件,并返回true。如果在寫入過程中發(fā)生了異常,則刪除新的文件,下一次從備份文件中恢復(fù)。
    try {
        FileOutputStream str = createFileOutputStream(mFile);//從File中獲取FileOutputStream
        if (str == null) {//獲取失敗,則取消寫入操作
            mcr.setDiskWriteResult(false);
            return;
        }
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);//借助XmlUtils工具,將MemoryCommitResult中保存的Map數(shù)據(jù),寫入到FileOutputStream中。
        FileUtils.sync(str);//執(zhí)行FileOutputStream的flush操作,同步到磁盤中

        str.close();
        ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);//設(shè)置文件的訪問模式
        try {
            final StructStat stat = Os.stat(mFile.getPath());
            synchronized (this) {
                mStatTimestamp = stat.st_mtime;//更新時(shí)間戳
                mStatSize = stat.st_size;//更新文件大小
            }
        } catch (ErrnoException e) {
            // Do nothing
        }
        //寫入成功了,刪除備份文件
        mBackupFile.delete();

        mDiskStateGeneration = mcr.memoryStateGeneration;//更新磁盤文件狀態(tài)

        mcr.setDiskWriteResult(true);//記錄寫入成功了,喚醒等待寫入結(jié)果的線程

        return;
    } catch (XmlPullParserException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    } catch (IOException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    }
    
    if (mFile.exists()) {
        if (!mFile.delete()) {
            Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
        }
    }
    mcr.setDiskWriteResult(false);
}

在writeToFile()方法中,首先將當(dāng)前文件重命名為備份文件,然后從當(dāng)前文件中獲取文件輸出流,并將MemoryCommitResult中保存的Map數(shù)據(jù),寫入到文件輸出流中。如果寫入成功,則刪除備份文件,返回true。如果寫入失敗,則刪除當(dāng)前文件,下一次從備份文件中恢復(fù)過來。

通知調(diào)用者是否寫入成功是通過setDiskWriteResult()方法來完成的,在該方法中,通過MemoryCommitResult的writeToDiskResult變量來保存寫入結(jié)果,寫入成功為true,寫入失敗為false。不管寫入成功還是失敗,都會(huì)讓writtenToDiskLatch閉鎖計(jì)數(shù)減1,喚醒在閉鎖上等待的線程。

public void setDiskWriteResult(boolean result) {
    writeToDiskResult = result;//保存是否寫入磁盤成功的結(jié)果
    writtenToDiskLatch.countDown();//減少閉鎖計(jì)數(shù),喚醒在閉鎖上等待的操作
}

既然有通過閉鎖計(jì)數(shù)減1,喚醒等待線程的操作,就應(yīng)該也有等待閉鎖計(jì)算計(jì)數(shù)為0的地方。這個(gè)地方,在調(diào)用commit()方法時(shí)候,會(huì)調(diào)用MemoryCommitResult上的閉鎖writtenToDiskLatch的await()方法。

try {
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    }

調(diào)用者獲取到寫入完成通知后,接著通知那些監(jiān)聽SharedPreference變化的監(jiān)聽者。具體是通過EditorImpl的notifyListeners()方法完成的。

2.13 EditorImpl.notifyListeners()

private void notifyListeners(final MemoryCommitResult mcr) {
    // 如果監(jiān)聽者為空,或者沒有修改過SharedPreference的內(nèi)容,則直接返回
    if (mcr.listeners == null || mcr.keysModified == null ||
        mcr.keysModified.size() == 0) {
        return;
    }
    //如果當(dāng)前線程是主線程,則把SharedPreference中的所有key修改通知給所有的監(jiān)聽者
    if (Looper.myLooper() == Looper.getMainLooper()) {
        for (int i = mcr.keysModified.size() - 1; i >= 0; i--) {
            final String key = mcr.keysModified.get(i);
            for (OnSharedPreferenceChangeListener listener : mcr.listeners) {
                if (listener != null) {
                    listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, key);
                }
            }
        }
    } else {
        // 如果是在子線程中,則讓該通知操作發(fā)生在主線程中
        ActivityThread.sMainThreadHandler.post(new Runnable() {
            public void run() {
                notifyListeners(mcr);
            }
        });
    }
}

在通知SharedPreference變化時(shí),首先判斷監(jiān)聽者是否空,或者SharedPreference是否發(fā)生了變化。然后在應(yīng)用主線程中將SharedPreference中的所有key修改通知給所有的監(jiān)聽者。

通過2.9~2.13的過程,描述了commit()的流程:

  1. 首先構(gòu)建一個(gè)寫入磁盤的輔助對(duì)象MemoryCommitResult,把mModified集合中的數(shù)據(jù)拷貝到mMap中,并把它保存到MemoryCommitResult的mapToWriteToDisk變量中;
  2. 如果當(dāng)前沒有寫入操作,則直接在當(dāng)前線程中執(zhí)行寫入操作;否則,封裝寫入操作到單線程任務(wù)隊(duì)列中,等待在其他線程中隨后執(zhí)行寫入操作;
  3. 寫入操作主要是將MemoryCommitResult中的mapToWriteToDisk集合內(nèi)容寫入到磁盤文件中,寫入完成后,再通過setDiskWriteResult()方法返回結(jié)果,并喚醒等待結(jié)果的線程;
  4. 等待寫入結(jié)果的線程被喚醒之后,通過notifyListeners()方法,在主線程中將SharedPreference中修改的key通知給監(jiān)聽者;
  5. 返回寫入結(jié)果給調(diào)用者;

2.14 EditorImpl.apply()

public void apply() {
    final MemoryCommitResult mcr = commitToMemory();//構(gòu)建需要寫入磁盤的數(shù)據(jù),見2.10
    final Runnable awaitCommit = new Runnable() {
            public void run() {
                try {
                    mcr.writtenToDiskLatch.await();//等待寫入完成
                } catch (InterruptedException ignored) {
                }
            }
        };

    QueuedWork.add(awaitCommit);//添加到等待結(jié)束任務(wù)隊(duì)列

    Runnable postWriteRunnable = new Runnable() {
            public void run() {
                awaitCommit.run();
                QueuedWork.remove(awaitCommit);//從等待結(jié)束任務(wù)隊(duì)列中移除
            }
        };

    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);//見2.11

    // 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);//通知監(jiān)聽者
}

可以看到apply()與commit()的主要區(qū)別是傳遞給enqueueDiskWrite()方法的第二個(gè)參數(shù)不同。在commit()方法中,postWriteRunnable為null,因此執(zhí)行的同步寫入操作,而在apply()方法中,postWriteRunnable不為null,因此apply()中的所有寫入操作都是在單線程的Executor中執(zhí)行。

在寫入操作完成后,會(huì)執(zhí)行postWriteRunnable里面的run()方法,在該run()方法中,又執(zhí)行awaitCommit里面的run()方法,在該run()方法中,主要是等待寫入操作完成。由于postWriteRunnable是在寫入操作完成后執(zhí)行的,因此該等待操作立即返回。

因?yàn)閍pply()方法的寫入操作,都是在單線程的Executor中執(zhí)行的,不能確切知道什么時(shí)候執(zhí)行完成。那么如果想等待異步操作完成后立即返回,該如何做呢?在QueuedWork中,有一個(gè)等待執(zhí)行結(jié)束的任務(wù)隊(duì)列,在執(zhí)行任務(wù)之前,先將任務(wù)添加到任務(wù)隊(duì)列中,等待任務(wù)執(zhí)行完成后,則再從任務(wù)隊(duì)列中移除該任務(wù)。

public class QueuedWork {
    private static final ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers =
        new ConcurrentLinkedQueue<Runnable>();

    /*
    * 添加一個(gè)任務(wù)到等待結(jié)束任務(wù)隊(duì)列中
    */
    public static void add(Runnable finisher) {
        sPendingWorkFinishers.add(finisher);
    }
    /*
    * 從等待結(jié)束任務(wù)隊(duì)列中移除該任務(wù)
    */
    public static void remove(Runnable finisher) {
        sPendingWorkFinishers.remove(finisher);
    }

    /*
    * 等待異步操作完成,如果異步操作沒有,則一直循環(huán)等待。
    */
    public static void waitToFinish() {
        Runnable toFinish;
        while ((toFinish = sPendingWorkFinishers.poll()) != null) {
            toFinish.run();
        }
    }
}

從QueuedWork中,可以看到,如果需要等待異步操作完成,只需在任務(wù)執(zhí)行前先通過QueuedWork.add()方法將任務(wù)添加到等待結(jié)束的任務(wù)隊(duì)列中,然后調(diào)用QueuedWork.waitToFinish()方法等待異步操作執(zhí)行完成,異步操作執(zhí)行完成后,會(huì)調(diào)用QueuedWork.remove()方法,從等待結(jié)束任務(wù)隊(duì)列中移除該任務(wù)。

可以看到在apply()方法中,在執(zhí)行異步寫入操作之前,通過QueuedWork.add()方法,將任務(wù)添加到了等待結(jié)束的任務(wù)隊(duì)列中,當(dāng)執(zhí)行完寫入操作后,再通過QueuedWork.remove()方法移除在結(jié)束等待任務(wù)隊(duì)列中的任務(wù)。

前面主要是介紹了數(shù)據(jù)的存儲(chǔ)過程,主要是借助Editor類的putXXX()方法來保存數(shù)據(jù),并最后通過commit()或apply()方法將內(nèi)存中的數(shù)據(jù)同步到磁盤中。

接下來看看SharedPreference是如何獲取數(shù)據(jù)的?

2.15 SharedPreferencesImpl.getString()

public String getString(String key, @Nullable String defValue) {
    synchronized (this) {
        awaitLoadedLocked();//等待SharedPreference加載到內(nèi)存中,見2.7
        String v = (String)mMap.get(key);//直接從mMap中獲取值
        return v != null ? v : defValue;//如果值不存在,則返回默認(rèn)的值
    }
}

可以看到,獲取數(shù)據(jù)的過程比較簡(jiǎn)單,首先是等待SharedPreference加載到內(nèi)存中,加載完成后,直接從mMap集合查看對(duì)應(yīng)key的value是否存在。如果存在,則直接返回,如果不存在,則返回默認(rèn)值。

在commitToMemory()方法中,我們可以看到,在寫入磁盤之前,其實(shí)已經(jīng)將數(shù)據(jù)先從mModified集合拷貝到mMap集合中。這樣做的一個(gè)目的是,當(dāng)一個(gè)線程執(zhí)行putXXX()操作后,另外一個(gè)線程就可以通過getXXX()立即獲得相關(guān)的值,因?yàn)檫@些數(shù)據(jù)都是保存在內(nèi)存中,可以立即返回,而不用等待數(shù)據(jù)寫入到磁盤后,再從磁盤中獲取數(shù)據(jù)。這是因?yàn)榇疟P操作是一個(gè)耗時(shí)的操作,所以通過mMap集合在內(nèi)存中緩存結(jié)果。

3.小結(jié)

  • SharedPreference主要用來保存一些簡(jiǎn)單的值,例如int、String、Boolean等類型。

  • SharedPreference數(shù)據(jù)的存儲(chǔ)必須通過Editor類的putXXX()方法進(jìn)行保存,然后通過Editor的commit()和apply()方法將數(shù)據(jù)同步到磁盤中。

  • SharedPreference數(shù)據(jù)的獲取可以直接通過SharedPreference的getXXX()方法進(jìn)行獲取。

  • SharedPreference的數(shù)據(jù)本質(zhì)上是保存在一個(gè)xml文件中,這個(gè)xml文件存放在/data/data/應(yīng)用包名/shared_prefs/目錄下。

  • 如果不需要數(shù)據(jù)寫入磁盤的結(jié)果,則可以使用apply()方法進(jìn)行磁盤寫入,該方法是在子線程中執(zhí)行。如果需要磁盤寫入結(jié)果,則可以使用commit()方法進(jìn)行磁盤寫入操作。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,595評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評(píng)論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,814評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,224評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評(píng)論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,444評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,988評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,804評(píng)論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,998評(píng)論 1 370
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評(píng)論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,237評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評(píng)論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,706評(píng)論 3 393
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,993評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容