SharedPreferences源碼分析學(xué)習(xí)

簡單使用

SharedPreferences sharedPreferences = getSharedPreferences("ouwen", Context.MODE_PRIVATE);
SharedPreferences.Editor edit = sharedPreferences.edit();
edit.putString("ouwen","123456");
保存數(shù)據(jù):edit.apply(); 或者 edit.commit()
讀取數(shù)據(jù):sharedPreferences.getString("ouwen","");

通常查看源碼都是帶著問題去分析, 避免在源碼里面迷失了,

那么關(guān)于SharedPreferences的幾點問題:

  • 怎么保存數(shù)據(jù)的?
  • 怎么讀取數(shù)據(jù)的?
  • 它是線程安全的嗎?
  • apply和commit都可以提交數(shù)據(jù),那么有什么區(qū)別?
  • 有什么需要注意的嗎?要怎么優(yōu)化?

怎么保存數(shù)據(jù)的?

先說結(jié)論再看源碼分析
本質(zhì)是把Map中的數(shù)據(jù)轉(zhuǎn)成xml文件, 以鍵值對的方式存儲在本地

  • 第一步: 獲取SharedPreferences實例
  • 第二步: 創(chuàng)建一個Map把我們當(dāng)前修改的數(shù)據(jù)存儲在內(nèi)存中
  • 第三步: 調(diào)用commit()或者apply() 把內(nèi)存中的數(shù)據(jù)存儲到本地

getSharedPreferences()源碼

public SharedPreferences getSharedPreferences(String name, int mode) {
    return mBase.getSharedPreferences(name, mode);
}

mBase: 是ContextImpl
傳入一個name和mode, 首先會嘗試從mSharedPrefsPaths中獲取name在本地的File, 如果為null, 就調(diào)用getSharedPreferencesPath(name) 創(chuàng)建一個name +".xml"的文件并且加入mSharedPrefsPaths緩存中, 繼續(xù)調(diào)用getSharedPreferences(file,mode)

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);
                ...
                sp = new SharedPreferencesImpl(file, mode);
                cache.put(file, sp);
                return sp;
            }
        }
        ...
        return sp;
    }

為了簡潔容易看, 刪除了部分代碼

  1. 先調(diào)用getSharedPreferencesCacheLocked()獲取File對應(yīng)的緩存cache, 這個ArrayMap是ConextImpl里的一個靜態(tài)成員屬性
  2. 第一次緩存是null, 所以會調(diào)用new SharedPreferencesImpl(file, mode) 創(chuàng)建一個實例加入緩存中并且返回

然后調(diào)用edit()方法獲取Editor的實例EditorImpl

 @Override
    public Editor edit() {
        ...
        synchronized (mLock) {
            awaitLoadedLocked();  這里會阻塞等待,直到讀取文件完成
        }
        return new EditorImpl();
    }

public final class EditorImpl implements Editor {
        ...
        private final Map<String, Object> mModified = new HashMap<>();
        @Override
        public Editor putString(String key, @Nullable String value) {
            synchronized (mEditorLock) {
                mModified.put(key, value);
                return this;
            }
        }
        @Override
        public Editor putInt(String key, int value) {
            synchronized (mEditorLock) {
                mModified.put(key, value);
                return this;
            }
        }
        ...
}

拿到SharedPreferences.Editor后就可以往里面putString(), putInt 等等...

哦~~~原來我們put的數(shù)據(jù)都是保存在mModified這個Map集合里面了,
這個時候數(shù)據(jù)還是在內(nèi)存里面, 只有調(diào)用commit()或者apply()才會保存到本地文件, 那就繼續(xù)看源碼吧

先對比一下commit() 和 apply() 源碼上有什么區(qū)別?

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

 public void apply() {
            ...
            final MemoryCommitResult mcr = commitToMemory();
            final Runnable awaitCommit = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            mcr.writtenToDiskLatch.await();  等待提交完成
                        } catch (InterruptedException ignored) {
                        }
                    }
                };
            //把這個等待提交的Runnable, 加入到QueuedWork中
            QueuedWork.addFinisher(awaitCommit);
            Runnable postWriteRunnable = new Runnable() {
                    @Override
                    public void run() {
                        awaitCommit.run(); 執(zhí)行上面的Runnable
                        //從QueuedWork中移出等待提交的Runnable
                        QueuedWork.removeFinisher(awaitCommit); 
                    }
                };
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
            notifyListeners(mcr);
        }

相同點:

  • 都會調(diào)用commitToMemory(), 然后調(diào)用enqueueDiskWrite()把Map的數(shù)據(jù)寫入本地文件中

不同點:

  • commit() 有返回值, 可以知道成功還是失敗
  • apply() 沒有返回值, 并且多了2個Runnable

commitToMemory()方法會遍歷之前put數(shù)據(jù)的mModified這個Map, 把我們修改的數(shù)據(jù)同步到另外一個Map中, 并且清空mModified自己

enqueueDiskWrite()

 private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {

        commit()方法調(diào)用enqueueDiskWrite的時候, 這個postWriteRunnable為空
        final boolean isFromSyncCommit = (postWriteRunnable == null);
        final Runnable writeToDiskRunnable = new Runnable() {
                @Override
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (mLock) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                writeToDiskRunnable.run();  這里會在主線程執(zhí)行, 寫入本地文件
                return;
            }
        }

        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

創(chuàng)建一個writeToDiskRunnable里面會執(zhí)行當(dāng)writeToFile()把數(shù)據(jù)寫入文件
調(diào)用commit的時候, isFromSyncCommit = true, 所以會當(dāng)前線程執(zhí)行
調(diào)用apply的時候會把寫入文件的Runnable加入QueuedWork.queue()中執(zhí)行, 其實是把Runnable發(fā)送到HandlerThread的子線程中執(zhí)行

 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);
            }
        }
}

private static Handler getHandler() {
        synchronized (sLock) {
            if (sHandler == null) {
                HandlerThread handlerThread = new HandlerThread("queued-work-looper",
                        Process.THREAD_PRIORITY_FOREGROUND);
                handlerThread.start();
                sHandler = new QueuedWorkHandler(handlerThread.getLooper());
            }
            return sHandler;
        }
}

怎么讀取數(shù)據(jù)的?

我們的程序啟動后,當(dāng)在某個地方想從SharedPreferences 里面獲取數(shù)據(jù)的時候
會調(diào)用下面代碼:

SharedPreferences sharedPreferences = getSharedPreferences("name", Context.MODE_PRIVATE);
sharedPreferences.getString(key,"");
  • 獲取SharedPreferences實例
  • 調(diào)用getString(key,"") 方法獲取數(shù)據(jù)

第一次調(diào)用的時候, 會調(diào)用new SharedPreferencesImpl()來創(chuàng)建SharedPreferences 的實例, 也就是在SharedPreferencesImpl的構(gòu)造方法里, 會從本地的xml文件加載數(shù)據(jù)到內(nèi)存

 SharedPreferencesImpl(File file, int mode) {
        ...
        mLoaded = false;
        mMap = null;
        startLoadFromDisk();
}

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

private void loadFromDisk() {
        synchronized (mLock) {
            if (mLoaded) {
                return;
            }
            ...
        }
        try {
           ...
           BufferedInputStream str = new BufferedInputStream(
                            new FileInputStream(mFile), 16 * 1024);
           map = (Map<String, Object>) XmlUtils.readMapXml(str);
        } catch (Throwable t) {
            thrown = t;
        }

        synchronized (mLock) {
            mLoaded = true;
            try {
                if (thrown == null) {
                    if (map != null) {
                        mMap = map;
                    } else {
                        mMap = new HashMap<>();
                    }
                }
            } catch (Throwable t) {
                mThrowable = t;
            } finally {
                mLock.notifyAll();  加載完成, 通知正在等待的代碼繼續(xù)執(zhí)行
            }
        }
    }

startLoadFromDisk()方法會啟動一個線程, 然后異步調(diào)用loadFromDisk()把xml文件的內(nèi)容加載到內(nèi)存Map中

思考: new Thread? 為啥不用線程池呢?
其實不是所有需要創(chuàng)建線程都用線程池來實現(xiàn), 當(dāng)我們確認(rèn)了調(diào)用的次數(shù)很少的時候,直接new Thread也是可以的

 public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();  阻塞等待加載文件完成
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
}

思考: 讀取本地文件是異步的, 那么如果保證數(shù)據(jù)正確?
在getString()方法中可以看到, 先會調(diào)用awaitLoadedLocked(), 再從mMap中根據(jù)key獲取value. 這個awaitLoadedLocked()會判斷當(dāng)前文件是否下載完成, 如果沒有完成就阻塞等待; 上面異步加載文件的loadFromDisk()方法, 在加載完成后會調(diào)用mLock.notifyAll()通知這里繼續(xù)執(zhí)行

 private void awaitLoadedLocked() {
        ...
        while (!mLoaded) {
            try {
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
    }

有什么需要注意的嗎? 要怎么優(yōu)化?

SP表示SharedPreferences

1.apply()也有可能導(dǎo)致卡頓甚至ANR

  final Runnable awaitCommit = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            mcr.writtenToDiskLatch.await();  等待提交完成
                        } catch (InterruptedException ignored) {
                        }
                    }
                };
      //把這個等待提交的Runnable, 加入到QueuedWork中
      QueuedWork.addFinisher(awaitCommit);

apply()雖然在子線程寫入文件, 但是在提交的時候會把a(bǔ)waitCommit加入到QueuedWork中, 并且在ActivityThread.handlePauseActivity(), ActivityThread.handleStopActivity(), ActivityThread.handleStopService(), ActivityThread.handleSleeping() 等等多處地方都會調(diào)用QueuedWork.waitToFinish()去阻塞等待SP保存數(shù)據(jù)完成, 也就是說如果apply()執(zhí)行寫入文件, Activity會等待它執(zhí)行完成才能關(guān)閉頁面...

更多關(guān)于 apply() 的問題, 可以看一下字節(jié)跳動技術(shù)團(tuán)隊的這篇文章

2.不要保存比較大的內(nèi)容, 會導(dǎo)致卡頓
因為SP加載的key和value以及put的內(nèi)容, 會一直保存在內(nèi)存當(dāng)中,占有內(nèi)存
第一次從SP中獲取數(shù)據(jù)的時候, 如果文件過大可能會導(dǎo)致卡頓,雖然讀取文件是在子線程中執(zhí)行, 但是看getString的源碼知道, 會調(diào)用awaitLoadedLocked阻塞在當(dāng)前線程等讀取文件完成

優(yōu)化: 可以提前在子線程先初始化SP, 過一段時間再去getString()獲取數(shù)據(jù)

3.不相關(guān)的配置, 不要保存在同一個文件中
因為文件越大加載越慢, 從而導(dǎo)致保存在內(nèi)存的數(shù)據(jù)越多, 如果有的配置用的比較頻繁,有的又很少用到,那分開文件存儲可以提高一點性能(文件比較小可以忽略)

4.避免頻繁提交, 盡量批量修改一起提交
因為寫入本地文件的時候會加鎖, 并且可能阻塞線程, 所以要避免頻繁提交,提交使用apply()

5.最好不要用來跨進(jìn)程使用
SP無法保證跨進(jìn)程使用時數(shù)據(jù)正常

如果項目對性能要求比較高, 或者想要跨進(jìn)程使用
可以看下微信的MMKV, 使用也很簡單, 聽說性能高,穩(wěn)定性強(qiáng)(暫時沒用過= =)
微信MMKV項目地址

小結(jié):

  • 怎么保存數(shù)據(jù)的?
    修改的數(shù)據(jù)會存儲內(nèi)存Map中,當(dāng)調(diào)用commit或者apply,就把數(shù)據(jù)寫入本地文件

  • 怎么讀取數(shù)據(jù)的?
    第一次會創(chuàng)建子線程從本地文件讀取到內(nèi)存Map中, 所以第一次稍微慢一點, 后面的都在內(nèi)存操作所以比較快

  • 它是線程安全的嗎?
    是線程安全的, 因為所有的讀和寫操作都加了synchronized,保證線程安全

  • apply和commit都可以提交數(shù)據(jù),有什么區(qū)別?
    commit有返回值,可以馬上知道提交結(jié)果, 但是commit是在當(dāng)前線程執(zhí)行寫入文件操作的
    apply沒有返回值, 是在HandlerThread的子線程中寫入到文件

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末椎工,一起剝皮案震驚了整個濱河市荣赶,隨后出現(xiàn)的幾起案子票髓,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件引瀑,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)方援,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來涛癌,“玉大人犯戏,你說我怎么就攤上這事∪埃” “怎么了先匪?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長弃衍。 經(jīng)常有香客問我呀非,道長,這世上最難降的妖魔是什么镜盯? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任姜钳,我火速辦了婚禮,結(jié)果婚禮上形耗,老公的妹妹穿的比我還像新娘哥桥。我一直安慰自己,他們只是感情好激涤,可當(dāng)我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布拟糕。 她就那樣靜靜地躺著判呕,像睡著了一般。 火紅的嫁衣襯著肌膚如雪送滞。 梳的紋絲不亂的頭發(fā)上侠草,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天,我揣著相機(jī)與錄音犁嗅,去河邊找鬼边涕。 笑死,一個胖子當(dāng)著我的面吹牛褂微,可吹牛的內(nèi)容都是我干的功蜓。 我是一名探鬼主播,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼宠蚂,長吁一口氣:“原來是場噩夢啊……” “哼式撼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起求厕,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤著隆,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后呀癣,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體美浦,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年项栏,在試婚紗的時候發(fā)現(xiàn)自己被綠了抵代。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡忘嫉,死狀恐怖荤牍,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情庆冕,我是刑警寧澤康吵,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站访递,受9級特大地震影響晦嵌,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜拷姿,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一惭载、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧响巢,春花似錦描滔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽券腔。三九已至,卻和暖如春拘泞,著一層夾襖步出監(jiān)牢的瞬間纷纫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工陪腌, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留辱魁,地道東北人。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓诗鸭,卻偏偏與公主長得像染簇,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子只泼,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,828評論 2 345