Android SharedPreferences解析

基于Api29源碼

SharedPreferences接口

首先,讓我們看下SharedPreferences接口


SharedPreferences接口

其中有兩個子接口 EditorOnSharedPreferenceChangeListener。我們發(fā)現(xiàn)SharedPreferences接口有很多getXXX系列的方法,通過這些方法可以獲得我們存進去的key對應的value。其中子接口Editor有很多putXXX系列的方法,我們可以利用這些方法為指定的key設置對應的value。需要注意的是,調用一系列的putXXX方法后如果沒有調用apply和commit是不會生效的。所以正常的使用規(guī)則通常類似 SharedPreferences對象.edit().putBoolean("xxx",false).putString("yyy").apply();。

其中 commit 方法是有返回提交成功還是失敗的,通常是同步調用(特殊情況下面有分析)。所以如果我們在主線程同時不需要知道操作是否成功的話是可以直接調用 apply 方法進行異步提交的。

對于子接口 OnSharedPreferenceChangeListener ,其作用就是在SharedPreferences的key被修改時進行回調,我們也可以看到SharedPreferences接口有registerOnSharedPreferenceChangeListener和unregisterOnSharedPreferenceChangeListener這兩個方法進行回調的注冊以及取消注冊。

那我們怎么獲得SharedPreferences對象呢?

  1. Context#getSharedPreferences
    我們這里簡單分析下源碼。
    Context的實現(xiàn)類是ContextImpl,所以我們去ContextImpl里面找getSharedPreferences函數。注意我貼的源碼都會刪掉一些不影響閱讀的代碼。
    public SharedPreferences getSharedPreferences(String name, int mode) {
        File file;
        synchronized (ContextImpl.class) {
            if (mSharedPrefsPaths == null) {
                mSharedPrefsPaths = new ArrayMap<>();
            }
            file = mSharedPrefsPaths.get(name);
            if (file == null) {
                file = getSharedPreferencesPath(name); //1
                mSharedPrefsPaths.put(name, file);
            }
        }
        return getSharedPreferences(file, mode);
    }

首先看入參,需要一個name來代表SharedPreferences對象對應硬盤上的文件的文件名。其次需要一個文件mode,mode有四種取值:MODE_PRIVATE,MODE_WORLD_READABLE,MODE_WORLD_WRITEABLE,MODE_MULTI_PROCESS。我們只要用第一種,后面三個都是用于多進程,且Android 7.0之后用會丟異常,后面會有分析。第一種可以簡單理解為只有創(chuàng)建該文件的進程可以控制讀寫(其實如果多個進程有同一個userId是都可以處理該文件的)。

我們能看到ContextImp用了一個ArrayMap對象mSharedPrefsPaths進行數據緩存。該Map對象的key為name,value為對應的硬盤File對象。

這里我們分析第一次使用,是沒有緩存的,所以直接看注釋1處的代碼getSharedPreferencesPath。

    public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);
            if (sp == null) {
                checkMode(mode);  //1
                sp = new SharedPreferencesImpl(file, mode);
                cache.put(file, sp);
                return sp;
            }
        }
        ...
        return sp;
    }

可以看到這里又有一個ArrayMap的緩存對象cache。這個Map對象的key為File對象,value為SharedPreferencesImpl對象。SharedPreferencesImpl就是SharedPreferences的實現(xiàn)類。

其中注釋1處的checkMode方法會檢查android版本是否大于等于7.0,如果是的話mode為MODE_WORLD_READABLE或MODE_WORLD_WRITEABLE會丟異常。

    private void checkMode(int mode) {
        if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {
            if ((mode & MODE_WORLD_READABLE) != 0) {
                throw new SecurityException("MODE_WORLD_READABLE no longer supported");
            }
            if ((mode & MODE_WORLD_WRITEABLE) != 0) {
                throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");
            }
        }
    }

至此我們就知道了SharedPreferences的獲取過程,下一章節(jié)會繼續(xù)探究SharedPreferences使用過程中的源碼。

  1. Activity#getPreferences
    public SharedPreferences getPreferences(int mode) {
        return getSharedPreferences(getLocalClassName(), mode);
    }

Activity繼承于Context的,內部也是調用了上面的getSharedPreferences方法,只不過文件名是Activity的名稱。

SharedPreferences源碼解析

創(chuàng)建過程

首先看下構造函數

    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file); //File對象,從名字看得出這個是備份文件
        mMode = mode;
        mLoaded = false; //Boolean對象 判斷是否已經從硬盤文件加載數據到內存
        mMap = null; //Map<String, Object>對象 硬盤文件存的key-value會放到該內存對象中
        startLoadFromDisk();
    }

我們直接看startLoadFromDisk方法

    private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }

可以看到其開啟一個子線程去加載讀取硬盤文件,我們繼續(xù)往下看

    private void loadFromDisk() {
        synchronized (mLock) {
            if (mLoaded) {
                //已經加載過就直接return
                return;
            }
            if (mBackupFile.exists()) {
                mFile.delete();
                mBackupFile.renameTo(mFile);
            }
        }

        Map<String, Object> map = null;
        if (mFile.canRead()) {
            BufferedInputStream str  = new BufferedInputStream(
                new FileInputStream(mFile), 16 * 1024);
            map = (Map<String, Object>) XmlUtils.readMapXml(str);
        }
        mLoaded = true; //將標記位設置true表示已經加載過硬盤數據到內存
        if (map != null) {
            mMap = map;
        } else {
            mMap = new HashMap<>();
        }
        //如果在loadFromDisk未執(zhí)行完的時候調用putXXX、getXXX系列方法
        //會執(zhí)行mLock.wait()進行等待,所以執(zhí)行完后將那些wait的線程喚醒。
        mLock.notifyAll();
    }

上面的代碼就是讀取本地文件,當一個xml來解析獲得一個map對象,然后賦值給成員變量mMap。

獲取數據過程

Ok,知道了創(chuàng)建過程,讓我們看看獲取value的過程。
一些列的getXXX方法我們只需要分析一個就好。
這里我們開始分析getInt方法。

    public int getInt(String key, int defValue) {
        synchronized (mLock) {
            awaitLoadedLocked(); //等待loadFromDisk執(zhí)行完
            Integer v = (Integer)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

其中awaitLoadedLocked會等待到子線程執(zhí)行完loadFromDisk。

    private void awaitLoadedLocked() {
        while (!mLoaded) {
            mLock.wait();
        }
    }

獲取數據的過程很簡單,就是通過內存對象mMap進行操作,既然是通過內存對象進行操作,那這里就有需要注意的一點,我們不應該對mMap取出來的對象進行修改,這樣的話其他線程再次從mMap中獲取數據的話,取出來的就是我們修改過后的數據!

設置數據過程

設置數據稍微麻煩一點,必須要先調用edit方法獲得Editor對象。

    public Editor edit() {
        synchronized (mLock) {
            awaitLoadedLocked();
        }
        return new EditorImpl();
    }

EditorImpl是Editor接口的實現(xiàn)類,我們看下其大概結構。

    public final class EditorImpl implements Editor {
        private final Object mEditorLock = new Object();//同步鎖
        //所有的putXXX系列方法調用都會先用該對象存儲
        private final Map<String, Object> mModified = new HashMap<>();
        private boolean mClear = false;//清除標記
    }

putXXX系列方法也基本一樣,所以這里我們分析下putStringSet就好了。

    public Editor putStringSet(String key, Set<String> values) {
        synchronized (mEditorLock) {
            mModified.put(key,
                    (values == null) ? null : new HashSet<String>(values));
            return this;
        }
    }

可以看到我們所有的putXXX系列方法中都將要改變的key和value存到了mModified對象中,其會在我們調用commit和apply方法時統(tǒng)一作用到mMap內存對象再提交到硬盤上。

提交數據過程

接下來我們分析下commit和apply方法

    public boolean commit() {
        MemoryCommitResult mcr = commitToMemory();
        SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
        mcr.writtenToDiskLatch.await();
        notifyListeners(mcr);
        return mcr.writeToDiskResult;
    }

    public void apply() {
        final MemoryCommitResult mcr = commitToMemory();
        final Runnable awaitCommit = () -> mcr.writtenToDiskLatch.await();
        QueuedWork.addFinisher(awaitCommit);
        Runnable postWriteRunnable = () -> {
            awaitCommit.run();
            QueuedWork.removeFinisher(awaitCommit);
        };
        SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
        notifyListeners(mcr);
    }

我們分析下兩個方法的區(qū)別,其都調用了commitToMemory方法、enqueueDiskWrite方法、notifyListeners方法。

notifyListeners很簡單,就是通知OnSharedPreferenceChangeListener進行回調。

commitToMemory主要就是將mModified的數據依次應用到mMap上。需要注意的是其會在一開始判斷你是否用Editor對象調用過clear方法,如果調用過的話他會先將mMap數據清空,再依次將mModified的數據依次應用到mMap上。比如執(zhí)行如下代碼后SharedPreferences.editor().putString("xx","1").clear().commit() ,mMap里就只有一個為xx的key啦。

Ok,重點就是enqueueDiskWrite方法。

    private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        //如果是commit方法,postWriteRunnable為null
        final boolean isFromSyncCommit = (postWriteRunnable == null);

        final Runnable writeToDiskRunnable = new Runnable() {
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        //將數據寫到文件
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (mLock) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };

        if (isFromSyncCommit) {
            //只有commit方式才會走到這
            boolean wasEmpty = false;
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                writeToDiskRunnable.run();
                return;
            }
        }

        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }

我們先分析apply方式,他會調用QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
我們直接看queue方法。

    public static void queue(Runnable work, boolean shouldDelay) {
        Handler handler = getHandler();

        synchronized (sLock) {
            sWork.add(work);

            if (shouldDelay && sCanDelay) {
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
            } else {
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
    }

如果是apply形式的話,shouldDely為true,其中getHandler獲得的Handler對象是通過一個HandlerThread中的Looper創(chuàng)建的,簡單說這個Handler對象是負責將消息發(fā)送到子線程的。DELAY是100ms,所以通過apply方法是會將上面的writeToDiskRunnable對象放到子線程延遲100ms執(zhí)行。換句話說是異步的。

最后我們看看commit方式,commit方法稍微復雜點,其涉及到mDiskWritesInFlight變量。這個變量在commitToMemory時會+1,writeToDiskRunnable中執(zhí)行完writeToFile后會-1,所以我們就把他理解為要寫到硬盤的次數就行!
我多舉幾個例子。
例子1:我調用commit時,會走到commitToMemory,其 mDiskWritesInFlight+1=1表示需要一次寫到硬盤,然后執(zhí)行enqueueDiskWrite方法,在這里面由于我是commit形式調用,其mDiskWritesInFlight又等于1,wasEmpty標記位為true,所以就直接同步執(zhí)行writeToDiskRunnable.run();。
例子2:我調用commit時(線程1),會走到commitToMemory,其 mDiskWritesInFlight+1=1表示需要一次寫到硬盤,這個時候我在另一個線程(線程2)又創(chuàng)建另一個editor對象并且調用commit,線程2走到commitToMemory,其其 mDiskWritesInFlight+1=2!,這個時候盡管是commit方式,mDiskWritesInFlight不等于1,所以wasEmpty為false,所以會走到QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);。我們知道queue方法是將writeToDiskRunnable放到子線程執(zhí)行的!所以,commit形式并不一定是同步執(zhí)行的!

后記

關于mBackupFile

還記得mBackupFile對象么。這個其實就是防止將內存數據寫到硬盤數據失敗的一種回退機制。
mBackupFile對象主要用在writeToFile方法中,我簡單截取相關代碼。

    private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
        //在這個方法中我們主要是通過mFile獲取一個輸出流用來將內存數據寫入硬盤,所以會先將之前的數據備份
        boolean backupFileExists = mBackupFile.exists();
        if (!backupFileExists) {
            //如果不存在mBackupFile,則將mFile重命名為mBackupFile進行備份
            mFile.renameTo(mBackupFile)
        } else {
            //如果存在mBackupFile,即已經有備份文件了,直接刪掉mFile即可
            mFile.delete();
        }

        
        FileOutputStream str = createFileOutputStream(mFile);
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);

        //如果上面寫入操作沒有丟異常會走到這里刪除備份文件
        mBackupFile.delete();
    }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容