SharedPreferences原理淺析

SharedPreferences原理淺析

1.概述

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

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

修改preferences必須通過一個Editor對象來確保存儲數(shù)據(jù)的一致性以及控制數(shù)據(jù)何時存儲链蕊。

SharedPreferences是一個接口请祖,里面定義了很多數(shù)據(jù)存儲與獲取的接口您机。

public interface SharedPreferences {
    /*
    * 定義了一個回調(diào)接口穿肄,當(dāng)SharedPreference被修改時,觸發(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()方法返回一個SharedPreferences實現(xiàn)某宪,該實現(xiàn)是SharedPreferencesImpl類仿村。接下來將來分析SharedPreferencesImpl實現(xiàn)類。

2.源碼分析

2.1 ContextImpl.getSharedPreferences()

public SharedPreferences getSharedPreferences(String name, int mode) {
    // 在Android 4.4以前兴喂,如果name為null的話蔼囊,則會把它當(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查找對應(yīng)的File文件是否存在焚志,不存在,則根據(jù)name創(chuàng)建一個File文件畏鼓,并把該File文件保存到一個Map集合中酱酬,以備后續(xù)使用
        file = mSharedPrefsPaths.get(name);
        if (file == null) {
            file = getSharedPreferencesPath(name);
            mSharedPrefsPaths.put(name, file);
        }
    }
    return getSharedPreferences(file, mode);//見2.2
}

在ContextImpl中定義了兩個與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;

我們在查找一個SharedPreferences時膳沽,首先需要根據(jù)SharedPreferences的name在mSharedPrefsPaths中查找到對應(yīng)的File文件,然后根據(jù)當(dāng)前應(yīng)用包名package name让禀,在sSharedPrefsCache中查找當(dāng)前應(yīng)用包名對應(yīng)的ArrayMap<File, SharedPreferencesImpl>挑社,最后根據(jù)File文件,查找對應(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)用包名,獲取對應(yīng)的ArrayMap<File, SharedPreferencesImpl>
        sp = cache.get(file);//根據(jù)文件腮敌,獲取對應(yīng)的SharedPreferencesImpl
        if (sp == null) {
            sp = new SharedPreferencesImpl(file, mode);//創(chuàng)建一個新的ShardPreferenceImpl對象阱当,見2.3
            cache.put(file, sp);//存入緩存中
            return sp;
        }
    }
    // 如果是多進程模式,或者是Android 3.0以前糜工,則檢測是否有其他進程在后臺修改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)用讀取這個模式創(chuàng)建的文件捌木。在Android N上使用該模式將拋出SecurityException異常表箭。
  • MODE_WORLD_WRITEABLE:允許其他應(yīng)用寫入這個模式創(chuàng)建的文件。在Android N上使用該模式將拋出SecurityException異常钮莲。
  • MODE_APPEND:如果文件已經(jīng)存在了免钻,則在文件的尾部添加數(shù)據(jù)。
  • MODE_MULTI_PROCESS:SharedPreference加載標(biāo)志崔拥,當(dāng)設(shè)置了該標(biāo)志极舔,則在磁盤上的文件將會被檢查是否修改了,盡管SharedPreference實例已經(jīng)在該進程中被加載了链瓦。

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

在獲取SharedPreference時,不是每次都會重新創(chuàng)建一個新的SharedPreference實例贴膘,而是先從緩存中卖子,查找是否存在對應(yīng)的SharedPreference實例,如果有相應(yīng)的實例刑峡,則直接返回洋闽。如果不存在玄柠,則創(chuàng)建一個新的SharedPreference實例,并把它保存在緩存中诫舅,以備下次使用羽利。

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)用包名對應(yīng)的ArrayMap<File, SharedPreferencesImpl>
    if (packagePrefs == null) {
        packagePrefs = new ArrayMap<>();
        sSharedPrefsCache.put(packageName, packagePrefs);//保存到緩存中
    }

    return packagePrefs;
}

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

可以看到户矢,SharedPreferencesImpl只會被創(chuàng)建一次,之后會被保存在緩存中殉疼,后續(xù)的獲取操作都是從緩存中獲取SharedPreferencesImpl實例對象梯浪。

2.3 SharedPreferencesImpl()

SharedPreferencesImpl(File file, int mode) {
    mFile = file;//保存SharedPreference對應(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)建一個線程來執(zhí)行從磁盤中加載數(shù)據(jù)
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();// 見2.5
        }
    }.start();
}

該方法主要是創(chuàng)建一個子線程虏劲,然后在子線程中執(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集合是一個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;//記錄時間戳
            mStatSize = stat.st_size;//記錄文件大小
        } else {
            mMap = new HashMap<>();
        }
        notifyAll();//喚醒被阻塞的線程
    }
}

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

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

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

2.6 SharedPreferencesImpl.edit()

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

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

在創(chuàng)建Editor之前百新,需要等待SharedPreferencesImpl加載到內(nèi)存中企软,然后才會創(chuàng)建一個Editor實現(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()方法喚醒等待的線程。

因為從磁盤中加載SharedPreference數(shù)據(jù)到內(nèi)存中是一個耗時操作斟珊,因此需要在子線程中執(zhí)行加載操作苇倡,當(dāng)子線程加載完成后,需要給主線程發(fā)送一個通知囤踩,喚醒被阻塞等待的操作旨椒。

在開始創(chuàng)建SharedPreferencesImpl時,就會從磁盤中加載xml文件到內(nèi)存中堵漱,加載完成后钩乍,將mLoaded置為true,并喚醒正在等待的線程怔锌。因為在通過Context.getSharedPreferences()獲取到SharedPreferencesImpl時寥粹,此時有可能數(shù)據(jù)并未全部都從磁盤加載到內(nèi)存中,因此需要在操作SharedPreferencesImpl之前埃元,等待數(shù)據(jù)從磁盤加載到內(nèi)存中涝涤。awaitLoadedLocked()操作就是用來完成等待數(shù)據(jù)從磁盤加載到內(nèi)存中,該方法返回后岛杀,可以確保所有的數(shù)據(jù)都加載到內(nèi)存中了阔拳,后續(xù)的所有操作都是針對內(nèi)存中數(shù)據(jù)進行操作了。

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

2.8 EditorImpl.putString()

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

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

private final Map<String, Object> mModified = Maps.newHashMap();//暫時保存需要寫入磁盤的數(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ù)寫入到磁盤時拯田,需要借助MemoryCommitResult類历造,構(gòu)建寫入磁盤的數(shù)據(jù)。然后將這個寫入操作由子線程來執(zhí)行船庇,并等待子線執(zhí)行完成吭产。當(dāng)子線程寫入完成后或者發(fā)生了異常,通知等待者寫入完成了溢十,并把寫入結(jié)果返回給調(diào)用者垮刹。

2.10 EditorImpl.commitToMemory()

private MemoryCommitResult commitToMemory() {
    MemoryCommitResult mcr = new MemoryCommitResult();//創(chuàng)建MemoryCommitResult實例
    synchronized (SharedPreferencesImpl.this) {
        // 在準(zhǔn)備將內(nèi)存中的數(shù)據(jù)寫入到磁盤時,如果已經(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)注冊的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對應(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)建一個MemoryCommitResult對象遮咖,該對象封裝了一些寫入磁盤的狀態(tài);
  2. 對mMap集合做一個深拷貝造虏,并把它保存在MemoryCommitResult的mapToWriteToDisk變量中御吞;
  3. 如果注冊了OnSharedPreferenceChangeListener監(jiān)聽者,則創(chuàng)建一個ArrayList列表漓藕,來保存被修改的key列表陶珠;
  4. 如果設(shè)置了清除標(biāo)志位mClear,則先清空mMap集合享钞;
  5. 將EditorImpl中mModified集合中的數(shù)據(jù)拷貝到mMap集合中揍诽,如果key對應(yīng)value已經(jīng)存在了,則跳過拷貝。如果key對應(yīng)的value為null暑脆,則刪除該key對應(yīng)的Entry渠啤。
  6. 清空mModified集合,并返回創(chuàng)建的MemoryCommitResult對象添吗。

將mModified集合中的數(shù)據(jù)拷貝到mMap集合中沥曹,具有緩存的作用,如果應(yīng)用再次馬上查詢SharedPreference時根资,則可以先從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時想邦,如果之前的寫入操作都完成了的話裤纹,則mDiskWritesInFlight的值為1
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();//執(zhí)行run()方法,同步調(diào)用
            return;
        }
    }

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

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

QueuedWork.singleThreadExecutor()方法返回的是一個單個線程的執(zhí)行器Executor慢洋,里面有一個無界的隊列來保存執(zhí)行任務(wù)塘雳。這樣的話,可以保證任務(wù)是順序的執(zhí)行普筹,并且保證每次只有一個任務(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)前文件更舊時,才執(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)嘗試寫入文件時盈包,刪除備份文件沸呐,并返回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;//更新時間戳
                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声畏。不管寫入成功還是失敗,都會讓writtenToDiskLatch閉鎖計數(shù)減1姻成,喚醒在閉鎖上等待的線程插龄。

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

既然有通過閉鎖計數(shù)減1科展,喚醒等待線程的操作均牢,就應(yīng)該也有等待閉鎖計算計數(shù)為0的地方。這個地方才睹,在調(diào)用commit()方法時候徘跪,會調(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變化時,首先判斷監(jiān)聽者是否空寒亥,或者SharedPreference是否發(fā)生了變化邮府。然后在應(yīng)用主線程中將SharedPreference中的所有key修改通知給所有的監(jiān)聽者。

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

  1. 首先構(gòu)建一個寫入磁盤的輔助對象MemoryCommitResult褂傀,把mModified集合中的數(shù)據(jù)拷貝到mMap中,并把它保存到MemoryCommitResult的mapToWriteToDisk變量中腐宋;
  2. 如果當(dāng)前沒有寫入操作紊服,則直接在當(dāng)前線程中執(zhí)行寫入操作檀轨;否則胸竞,封裝寫入操作到單線程任務(wù)隊列中,等待在其他線程中隨后執(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ù)隊列

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

    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()方法的第二個參數(shù)不同浑测。在commit()方法中,postWriteRunnable為null歪玲,因此執(zhí)行的同步寫入操作迁央,而在apply()方法中,postWriteRunnable不為null滥崩,因此apply()中的所有寫入操作都是在單線程的Executor中執(zhí)行岖圈。

在寫入操作完成后,會執(zhí)行postWriteRunnable里面的run()方法钙皮,在該run()方法中蜂科,又執(zhí)行awaitCommit里面的run()方法顽决,在該run()方法中,主要是等待寫入操作完成导匣。由于postWriteRunnable是在寫入操作完成后執(zhí)行的擎值,因此該等待操作立即返回。

因為apply()方法的寫入操作逐抑,都是在單線程的Executor中執(zhí)行的鸠儿,不能確切知道什么時候執(zhí)行完成。那么如果想等待異步操作完成后立即返回厕氨,該如何做呢进每?在QueuedWork中,有一個等待執(zhí)行結(jié)束的任務(wù)隊列命斧,在執(zhí)行任務(wù)之前田晚,先將任務(wù)添加到任務(wù)隊列中,等待任務(wù)執(zhí)行完成后国葬,則再從任務(wù)隊列中移除該任務(wù)贤徒。

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

    /*
    * 添加一個任務(wù)到等待結(jié)束任務(wù)隊列中
    */
    public static void add(Runnable finisher) {
        sPendingWorkFinishers.add(finisher);
    }
    /*
    * 從等待結(jié)束任務(wù)隊列中移除該任務(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ù)隊列中,然后調(diào)用QueuedWork.waitToFinish()方法等待異步操作執(zhí)行完成背苦,異步操作執(zhí)行完成后互捌,會調(diào)用QueuedWork.remove()方法,從等待結(jié)束任務(wù)隊列中移除該任務(wù)行剂。

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

前面主要是介紹了數(shù)據(jù)的存儲過程壤躲,主要是借助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ù)的過程比較簡單客税,首先是等待SharedPreference加載到內(nèi)存中,加載完成后撕贞,直接從mMap集合查看對應(yīng)key的value是否存在更耻。如果存在,則直接返回捏膨,如果不存在秧均,則返回默認(rèn)值。

在commitToMemory()方法中号涯,我們可以看到目胡,在寫入磁盤之前,其實已經(jīng)將數(shù)據(jù)先從mModified集合拷貝到mMap集合中链快。這樣做的一個目的是誉己,當(dāng)一個線程執(zhí)行putXXX()操作后,另外一個線程就可以通過getXXX()立即獲得相關(guān)的值域蜗,因為這些數(shù)據(jù)都是保存在內(nèi)存中巨双,可以立即返回,而不用等待數(shù)據(jù)寫入到磁盤后霉祸,再從磁盤中獲取數(shù)據(jù)筑累。這是因為磁盤操作是一個耗時的操作,所以通過mMap集合在內(nèi)存中緩存結(jié)果脉执。

3.小結(jié)

  • SharedPreference主要用來保存一些簡單的值疼阔,例如int、String半夷、Boolean等類型。

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

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

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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末帆离,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子结澄,更是在濱河造成了極大的恐慌哥谷,老刑警劉巖岸夯,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異们妥,居然都是意外死亡猜扮,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門监婶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來旅赢,“玉大人,你說我怎么就攤上這事惑惶∠输觯” “怎么了?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵集惋,是天一觀的道長孕似。 經(jīng)常有香客問我,道長刮刑,這世上最難降的妖魔是什么喉祭? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮雷绢,結(jié)果婚禮上泛烙,老公的妹妹穿的比我還像新娘。我一直安慰自己翘紊,他們只是感情好蔽氨,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著帆疟,像睡著了一般鹉究。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上踪宠,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天自赔,我揣著相機與錄音,去河邊找鬼柳琢。 笑死绍妨,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的柬脸。 我是一名探鬼主播他去,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼倒堕!你這毒婦竟也來了灾测?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤涩馆,失蹤者是張志新(化名)和其女友劉穎行施,沒想到半個月后允坚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡蛾号,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年稠项,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鲜结。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡展运,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出精刷,到底是詐尸還是另有隱情拗胜,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布怒允,位于F島的核電站埂软,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏纫事。R本人自食惡果不足惜勘畔,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望丽惶。 院中可真熱鬧炫七,春花似錦、人聲如沸钾唬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽抡秆。三九已至奕巍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間琅轧,已是汗流浹背伍绳。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留乍桂,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓效床,卻偏偏與公主長得像睹酌,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子剩檀,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353

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