Android SharedPreferences該這樣優(yōu)化

前言

同學(xué)猎莲,聽(tīng)說(shuō)SharedPreference你玩的很6,不就是存儲(chǔ)鍵值對(duì)嘛技即,工具類就可以搞定著洼。那下面這些問(wèn)題,你都回答的上來(lái)嗎而叼?

目錄

1身笤、SharedPreference有哪些隱患或風(fēng)險(xiǎn)?
2葵陵、為什么SharedPreference會(huì)造成卡頓甚至ANR液荸?
3、如何解決sp造成的界面卡頓脱篙、掉幀問(wèn)題娇钱?
4、commit和apply有什么區(qū)別绊困?
5忍弛、apply就不會(huì)讓主線程卡頓?
6考抄、SharedPreference如何跨進(jìn)程通信?

還沒(méi)有看過(guò)源碼的同學(xué)蔗彤,強(qiáng)烈推薦先看一遍川梅,不然會(huì)有各種不適癥狀疯兼。
Android SharedPreferences源碼都不會(huì),怎么通過(guò)面試贫途?

SharedPreference有哪些隱患或風(fēng)險(xiǎn)吧彪?

卡頓、丟幀丢早、甚至ANR姨裸、占用內(nèi)存過(guò)高。

為什么SharedPreference會(huì)造成卡頓甚至ANR怨酝?

  • 第一次從SharedPreference獲取值的時(shí)候傀缩,可能阻塞主線程,造成卡頓/丟幀农猬。

看如下代碼赡艰,我第一次從sp取數(shù)據(jù)竟然花費(fèi)了11ms。這還是我的數(shù)據(jù)很少的情況下斤葱,很多時(shí)候慷垮,一個(gè)迭代了很多版本的項(xiàng)目存放的數(shù)據(jù)會(huì)遠(yuǎn)比我的要大,耗時(shí)也會(huì)更長(zhǎng)揍堕。

 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        var startTime = System.currentTimeMillis()
        val sp: SharedPreferences = getSharedPreferences("sp", Context.MODE_PRIVATE)
        val value: String? = sp.getString("key", "")
        Log.e(TAG, "func1  :  ${System.currentTimeMillis() - startTime}")
    }

有人會(huì)說(shuō)SharedPreferences 的加載是不是在子線程嗎料身,為什么還會(huì)阻塞主線程呢?這個(gè)問(wèn)題衩茸,我們要從源碼中尋找答案芹血。

public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
            //阻塞等待加載、解析xml文件完成
            awaitLoadedLocked();
            //從內(nèi)存獲取
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

從內(nèi)存獲取數(shù)據(jù)之前递瑰,還調(diào)用了 awaitLoadedLocked()祟牲,下面來(lái)看看這個(gè)方法。

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();
        }
        while (!mLoaded) {
            try {
                //關(guān)鍵點(diǎn)抖部,object對(duì)象的wait()來(lái)阻塞等待
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
    }

awaitLoadedLocked()會(huì)循環(huán)等待说贝,直到mLoaded為true,那什么時(shí)候mLoaded為true呢慎颗?答案是從磁盤(pán)加載乡恕、解析xml完成之后,具體是在SharedPreferencesImpl#loadFromDisk()方法內(nèi)俯萎,這里不展開(kāi)了傲宜,可以去上一篇源碼分析文章看。

小結(jié)夫啊,第一次獲取數(shù)據(jù)的時(shí)候會(huì)阻塞主線程函卒,原因是主線程會(huì)等待從文件加載sp完成,這是一個(gè)耗時(shí)操作撇眯,尤其是xml中數(shù)據(jù)比較大的時(shí)候更明顯报嵌;注意:只有第一次才會(huì)虱咧,后面不會(huì),因?yàn)榧虞d文件成功后會(huì)在內(nèi)存緩存數(shù)據(jù)锚国,下次就不需要等待了腕巡。

怎么解決?盡可能早的去完成sp對(duì)象的初始化血筑,通常在Application是最合適的绘沉。

多次commit、apply

我見(jiàn)過(guò)很多這樣的代碼,每次寫(xiě)入數(shù)據(jù)都會(huì)創(chuàng)建一個(gè)Editor對(duì)象豺总,調(diào)用一次commit/apply车伞。

 val sp: SharedPreferences = getSharedPreferences("sp", Context.MODE_PRIVATE)
 sp.edit().putString("key0", "11").apply()
 sp.edit().putString("key1", "11").apply()
 sp.edit().putString("key2", "11").apply()

創(chuàng)建Editor對(duì)象和put方法并不怎么耗時(shí),但是多次commit()/apply()有多耗時(shí)园欣,您心里沒(méi)數(shù)嗎帖世?不信就來(lái)看看下面這組數(shù)據(jù)。

存儲(chǔ)方式 數(shù)據(jù)量 耗時(shí)(ms)
多次commit 20 116
多次apply 20 5
一次性commit 20 6
一次性apply 20 1

所以沸枯,請(qǐng)把你的代碼改成這樣日矫。另外在不需要返回值的時(shí)候,請(qǐng)你使用apply()绑榴,官方也是這樣推薦的哪轿。

        val sp: SharedPreferences = getSharedPreferences("sp", Context.MODE_PRIVATE)
        var editor = sp.edit()
        editor.putString("key0", "11")
            .putString("key1", "11")
            .putString("key2", "11")
            .apply()

我們都知道commit()是在主線程寫(xiě)入文件的,很可能會(huì)卡頓甚至ANR翔怎。有人會(huì)說(shuō)窃诉,用apply()不就可以了嗎?

模擬面試
面試官:sp的apply()會(huì)造成卡頓嗎赤套?
小A:不會(huì)卡頓飘痛,commit才會(huì)卡頓,因?yàn)閍pply是在子線程寫(xiě)入文件的容握⌒觯【得意??】
面試官:你確定不會(huì)卡?
小A:我確定剔氏。
面試官:其實(shí)也可能會(huì)卡的塑猖。吧啦吧啦。谈跛。羊苟。「泻叮【得意??】
小A:大佬蜡励,原來(lái)還可以這樣,佛了。

下面就把面試官解釋的這一段補(bǔ)充完整巍虫。
先來(lái)看一下apply方法彭则,注釋有點(diǎn)多,可以幫到喜歡追求細(xì)節(jié)的朋友

public void apply() {
           //修改內(nèi)存緩存mMap
            final MemoryCommitResult mcr = commitToMemory();
            //等待寫(xiě)入文件完成的任務(wù)
            final Runnable awaitCommit = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            //阻塞等待寫(xiě)入文件完成,否則阻塞在這
                            //利用CountDownLatch來(lái)等待任務(wù)的完成
//后面執(zhí)行enqueueDiskWrite寫(xiě)入文件成功后會(huì)把writtenToDiskLatch多線程計(jì)數(shù)器減1占遥, 這樣的話下面的阻塞代碼就可以通過(guò)了.
                            mcr.writtenToDiskLatch.await();
                        } catch (InterruptedException ignored) {
                        }
                    }
                };
            
            //QueuedWork是用來(lái)確保SharedPrefenced的寫(xiě)操作在Activity 銷毀前執(zhí)行完的一個(gè)全局隊(duì)列. 
            //QueuedWork里面的隊(duì)列是通過(guò)LinkedList實(shí)現(xiàn)的,LinkedList不僅可以做鏈表输瓜,也可以做隊(duì)列
            //添加到全局的工作隊(duì)列中
            QueuedWork.addFinisher(awaitCommit);

          //這個(gè)任務(wù)是等待磁盤(pán)寫(xiě)入完成瓦胎,然后從隊(duì)列中移除任務(wù)
            Runnable postWriteRunnable = new Runnable() {
                    @Override
                    public void run() {
                        //執(zhí)行阻塞任務(wù)
                        awaitCommit.run();
                        //阻塞完成之后,從隊(duì)列中移除任務(wù)
                        QueuedWork.removeFinisher(awaitCommit);
                    }
                };
            //異步執(zhí)行磁盤(pán)文件寫(xiě)入尤揣,注意這里和commit不同的是postWriteRunnable不為空
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

       
            notifyListeners(mcr);
        }

關(guān)鍵點(diǎn)1:把一個(gè)帶有await的runnable添加進(jìn)了QueueWork類的一個(gè)隊(duì)列,注意一下這個(gè)sFinishers搔啊,等會(huì)兒會(huì)用到

//把任務(wù)添加到全局的工作隊(duì)列中
QueuedWork.addFinisher(awaitCommit);

public class QueuedWork {
    /** Finishers {@link #addFinisher added} and not yet {@link #removeFinisher removed} */
    private static final LinkedList<Runnable> sFinishers = new LinkedList<>();

    public static void addFinisher(Runnable finisher) {
        synchronized (sLock) {
            sFinishers.add(finisher);
        }
    }
}

關(guān)鍵點(diǎn)2:把寫(xiě)入文件的任務(wù)放入一個(gè)隊(duì)列中,在QueuedWork內(nèi)部會(huì)通過(guò)HandlerThread串行的執(zhí)行北戏。

//apply異步寫(xiě)入文件
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

到這里负芋,看上去還沒(méi)有問(wèn)題,在子線程寫(xiě)文件并不會(huì)造成UI線程卡頓嗜愈,但是我們來(lái)看一下ActivityThread的handleStopActivity方法旧蛾。

public void handleStopActivity(IBinder token, boolean show, int configChanges,
            PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {
      ...省略無(wú)關(guān)代碼
        // Make sure any pending writes are now committed.
        if (!r.isPreHoneycomb()) {
            QueuedWork.waitToFinish();
        }
    }

//如果sdk版本<11返回false
private boolean isPreHoneycomb() {
            return activity != null && activity.getApplicationInfo().targetSdkVersion
                    < android.os.Build.VERSION_CODES.HONEYCOMB;
        }

注意,在onPause會(huì)調(diào)用handleStopActivity()方法蠕嫁,而且?guī)缀醵紩?huì)執(zhí)行QueuedWork.waitToFinish();方法锨天。waitToFinish?看上去像是等待完成剃毒?

public static void waitToFinish() {
     
            while (true) {
                Runnable finisher;

                synchronized (sLock) {
                    //從隊(duì)列取任務(wù)
                    finisher = sFinishers.poll();
                }

                if (finisher == null) {
                    break;
                }

                finisher.run();
            }
    }

這個(gè)方法很簡(jiǎn)單病袄,循環(huán)地從sFinishers這個(gè)隊(duì)列中取任務(wù)執(zhí)行,直到任務(wù)為空赘阀。這個(gè)任務(wù)就是之前apply中的awaitCommit益缠,它是用來(lái)等待寫(xiě)入文件的線程執(zhí)行完畢的。現(xiàn)在試想一下基公,在onPause之后幅慌,如果因?yàn)槟愣啻问褂昧薬pply,那就意味著寫(xiě)入任務(wù)會(huì)在這里排隊(duì)酌媒,但是寫(xiě)入文件那里只有一個(gè)HandlerThread在串行的執(zhí)行欠痴,那是不是就卡頓了?

給出幾條建議:

  • 第1:不要多次apply秒咨,請(qǐng)合并為1次事物提交喇辽,因?yàn)镮/O真的很耗時(shí);
  • 第2:請(qǐng)你不要在sp存放太大的數(shù)據(jù)雨席,比如json之類菩咨,因?yàn)槲募蟪跏蓟瘯?huì)很耗時(shí),而且文件內(nèi)容會(huì)一直緩存在內(nèi)存中,得不到釋放抽米;
  • 第3:如果你的apply只是存儲(chǔ)了輕量級(jí)的數(shù)據(jù)特占,比如true、"abc"這樣的內(nèi)容云茸,請(qǐng)大膽的使用是目,沒(méi)有什么性能影響;
  • 第4:如果優(yōu)化了apply還出現(xiàn)卡頓标捺,就用commit吧懊纳,但是需要自己進(jìn)行異步處理,至于用Thread還是線程池或者其它看你自己業(yè)務(wù)亡容。

如何解決sp造成的界面卡頓嗤疯、掉幀問(wèn)題?

其實(shí)上面的分析已經(jīng)給出答案了闺兢,這里再總結(jié)一下茂缚。

  • 1.初始化sp放在application;
  • 2.不要頻繁的commit/apply,盡量使用一次事物提交屋谭;
  • 3.優(yōu)先選擇用apply而不是commit脚囊,因?yàn)閏ommit會(huì)卡UI;
  • 4.sp是輕量級(jí)的存儲(chǔ)工具戴而,所以請(qǐng)你不要存放太大的數(shù)據(jù)凑术,不要存json等;
  • 5.單個(gè)sp文件不要太大所意,如果數(shù)據(jù)量很大淮逊,請(qǐng)把關(guān)聯(lián)性比較大的,高頻操作的放在單獨(dú)的sp文件

commit和apply有什么區(qū)別扶踊?

  • commit()是同步且有返回值的泄鹏;apply()方法是異步?jīng)]有返回值的;
  • commit()在主線程寫(xiě)入文件秧耗,會(huì)造成UI卡頓备籽;apply()在子線程寫(xiě)入文件,也有可能卡UI分井;

SharedPreference如何跨進(jìn)程通信车猬?

有人寄希望于在初始化sp的時(shí)候,設(shè)置flag為MODE_MULTI_PROCESS來(lái)跨進(jìn)程通信尺锚,但是很遺憾珠闰,這種方式已經(jīng)被廢棄。

getSharedPreferences("sp", Context.MODE_MULTI_PROCESS)

* @deprecated MODE_MULTI_PROCESS does not work reliably in
     * some versions of Android, and furthermore does not provide any
     * mechanism for reconciling concurrent modifications across
     * processes.  Applications should not attempt to use it.  Instead,
     * they should use an explicit cross-process data management
     * approach such as {@link android.content.ContentProvider ContentProvider}.
     */
    @Deprecated
    public static final int MODE_MULTI_PROCESS = 0x0004;

如果要跨進(jìn)程通信瘫辩,需要在sp外面包裹一層ContentProvider伏嗜,當(dāng)然用mmkv性能上更佳坛悉。

感謝以下作者

Android SharedPreference源碼閱讀
SharedPreferences靈魂拷問(wèn)之原理
http://www.reibang.com/p/19f261f436ae
Android SharedPreferences.apply() 問(wèn)題

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市承绸,隨后出現(xiàn)的幾起案子裸影,更是在濱河造成了極大的恐慌,老刑警劉巖军熏,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件轩猩,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡羞迷,警方通過(guò)查閱死者的電腦和手機(jī)界轩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)衔瓮,“玉大人,你說(shuō)我怎么就攤上這事抖甘∪劝埃” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵衔彻,是天一觀的道長(zhǎng)薇宠。 經(jīng)常有香客問(wèn)我,道長(zhǎng)艰额,這世上最難降的妖魔是什么澄港? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮柄沮,結(jié)果婚禮上回梧,老公的妹妹穿的比我還像新娘。我一直安慰自己祖搓,他們只是感情好狱意,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著拯欧,像睡著了一般详囤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上镐作,一...
    開(kāi)封第一講書(shū)人閱讀 49,749評(píng)論 1 289
  • 那天藏姐,我揣著相機(jī)與錄音,去河邊找鬼该贾。 笑死羔杨,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的靶庙。 我是一名探鬼主播问畅,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼娃属,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了护姆?” 一聲冷哼從身側(cè)響起矾端,我...
    開(kāi)封第一講書(shū)人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎卵皂,沒(méi)想到半個(gè)月后秩铆,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡灯变,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年殴玛,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片添祸。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡滚粟,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出刃泌,到底是詐尸還是另有隱情凡壤,我是刑警寧澤,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布耙替,位于F島的核電站亚侠,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏俗扇。R本人自食惡果不足惜硝烂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望铜幽。 院中可真熱鬧滞谢,春花似錦、人聲如沸啥酱。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)镶殷。三九已至禾酱,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間绘趋,已是汗流浹背颤陶。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留陷遮,地道東北人滓走。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像帽馋,于是被迫代替她去往敵國(guó)和親搅方。 傳聞我的和親對(duì)象是個(gè)殘疾皇子比吭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348