<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          再見(jiàn)SharedPreferences,你好MMKV!(附中獎(jiǎng)名單)

          共 9138字,需瀏覽 19分鐘

           ·

          2021-04-27 12:27

           劉望舒 專注于大前端和Java領(lǐng)域的個(gè)人技術(shù)號(hào)
          公眾號(hào)回復(fù)Android加入安卓技術(shù)群

          作者:RicardoMJiang

          https://juejin.cn/post/6930168094983716877

          前言

          SharedPreferences是谷歌提供的輕量級(jí)存儲(chǔ)方案,使用起來(lái)比較方便,可以直接進(jìn)行數(shù)據(jù)存儲(chǔ),不必另起線程。


          不過(guò)也帶來(lái)很多問(wèn)題,尤其是由SP引起的ANR問(wèn)題,非常常見(jiàn)。


          正因如此,后來(lái)也出現(xiàn)了一些SP的替代解決方案,比如MMKV。


          本文主要包括以下內(nèi)容


          1.SharedPreferences存在的問(wèn)題

          2.MMKV的基本使用與介紹

          3.MMKV的原理

          1.SharedPreferences存在的問(wèn)題

          SP的效率比較低


          1.讀寫(xiě)方式:直接I/O


          2.數(shù)據(jù)格式:xml


          3.寫(xiě)入方式:全量更新



          由于SP使用的xml格式保存數(shù)據(jù),所以每次更新數(shù)據(jù)只能全量替換更新數(shù)據(jù)。


          這意味著如果我們有100個(gè)數(shù)據(jù),如果只更新一項(xiàng)數(shù)據(jù),也需要將所有數(shù)據(jù)轉(zhuǎn)化成xml格式,然后再通過(guò)io寫(xiě)入文件中。


          這也導(dǎo)致SP的寫(xiě)入效率比較低。


          commit導(dǎo)致的ANR



          public boolean commit() {
              // 在當(dāng)前線程將數(shù)據(jù)保存到mMap中
              MemoryCommitResult mcr = commitToMemory();
              SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
              try {
                  // 如果是在singleThreadPool中執(zhí)行寫(xiě)入操作,通過(guò)await()暫停主線程,直到寫(xiě)入操作完成。
                  // commit的同步性就是通過(guò)這里完成的。
                  mcr.writtenToDiskLatch.await();
              } catch (InterruptedException e) {
                  return false;
              }
              /*
               * 回調(diào)的時(shí)機(jī):
               * 1. commit是在內(nèi)存和硬盤(pán)操作均結(jié)束時(shí)回調(diào)
               * 2. apply是內(nèi)存操作結(jié)束時(shí)就進(jìn)行回調(diào)
               */

              notifyListeners(mcr);
              return mcr.writeToDiskResult;
          }



          如上所示


          1.commit有返回值,表示修改是否提交成功。


          2.commit提交是同步的,直到磁盤(pán)操作成功后才會(huì)完成。


          所以當(dāng)數(shù)據(jù)量比較大時(shí),使用commit很可能引起ANR。


          Apply導(dǎo)致的ANR


          commit是同步的,同時(shí)SP也提供了異步的apply。


          apply是將修改數(shù)據(jù)原子提交到內(nèi)存, 而后異步真正提交到硬件磁盤(pán), 而commit是同步的提交到硬件磁盤(pán),因此,在多個(gè)并發(fā)的提交commit的時(shí)候,他們會(huì)等待正在處理的commit保存到磁盤(pán)后在操作,從而降低了效率。


          而apply只是原子的提交到內(nèi)容,后面有調(diào)用apply的函數(shù)的將會(huì)直接覆蓋前面的內(nèi)存數(shù)據(jù),這樣從一定程度上提高了很多效率。


          但是apply同樣會(huì)引起ANR的問(wèn)題。



          public void apply() {
              final long startTime = System.currentTimeMillis();

              final MemoryCommitResult mcr = commitToMemory();
              final Runnable awaitCommit = new Runnable() {
                      @Override
                      public void run() {
                          mcr.writtenToDiskLatch.await(); // 等待
                          ......
                      }
                  };
              // 將 awaitCommit 添加到隊(duì)列 QueuedWork 中
              QueuedWork.addFinisher(awaitCommit);

              Runnable postWriteRunnable = new Runnable() {
                      @Override
                      public void run() {
                          awaitCommit.run();
                          QueuedWork.removeFinisher(awaitCommit);
                      }
                  };
              SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
          }



          • 將一個(gè) awaitCommit  Runnable 任務(wù),添加到隊(duì)列 QueuedWork 中,在 awaitCommit中會(huì)調(diào)用 await() 方法等待,在 handleStopService handleStopActivity 等等生命周期會(huì)以這個(gè)作為判斷條件,等待任務(wù)執(zhí)行完畢。


          • 將一個(gè) postWriteRunnable   Runnable 寫(xiě)任務(wù),通過(guò) enqueueDiskWrite 方法,將寫(xiě)入任務(wù)加入到隊(duì)列中,而寫(xiě)入任務(wù)在一個(gè)線程中執(zhí)行。


          為了保證異步任務(wù)及時(shí)完成,當(dāng)生命周期處于 handleStopService() handlePauseActivity()  handleStopActivity() 的時(shí)候會(huì)調(diào)用 QueuedWork.waitToFinish() 會(huì)等待寫(xiě)入任務(wù)執(zhí)行完畢。


          private static final ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers =
                  new ConcurrentLinkedQueue<Runnable>();

          public static void waitToFinish() {
              Runnable toFinish;
              while ((toFinish = sPendingWorkFinishers.poll()) != null) {
                  toFinish.run(); // 相當(dāng)于調(diào)用 `mcr.writtenToDiskLatch.await()` 方法
              }
          }


          • sPendingWorkFinishers ConcurrentLinkedQueue 實(shí)例,apply 方法會(huì)將寫(xiě)入任務(wù)添加到 sPendingWorkFinishers隊(duì)列中,在單個(gè)線程的線程池中執(zhí)行寫(xiě)入任務(wù),線程的調(diào)度并不由程序來(lái)控制,也就是說(shuō)當(dāng)生命周期切換的時(shí)候,任務(wù)不一定處于執(zhí)行狀態(tài)。


          • toFinish.run() 方法,相當(dāng)于調(diào)用 mcr.writtenToDiskLatch.await() 方法,會(huì)一直等待。


          • waitToFinish() 方法就做了一件事,會(huì)一直等待寫(xiě)入任務(wù)執(zhí)行完畢,其它什么都不做,當(dāng)有很多寫(xiě)入任務(wù),會(huì)依次執(zhí)行,當(dāng)文件很大時(shí),效率很低,造成 ANR 就不奇怪了。


          所以當(dāng)數(shù)據(jù)量比較大時(shí),apply也會(huì)造成ANR。


          getXXX() 導(dǎo)致ANR


          不僅是寫(xiě)入操作,所有 getXXX() 方法都是同步的,在主線程調(diào)用 get 方法,必須等待 SP 加載完畢,也有可能導(dǎo)致ANR。


          調(diào)用
          getSharedPreferences() 方法,最終會(huì)調(diào)用 SharedPreferencesImpl#startLoadFromDisk() 方法開(kāi)啟一個(gè)線程異步讀取數(shù)據(jù)。



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



          正如你所看到的,開(kāi)啟一個(gè)線程異步讀取數(shù)據(jù),當(dāng)我們正在讀取一個(gè)比較大的數(shù)據(jù),還沒(méi)讀取完,接著調(diào)用 getXXX() 方法。



          public String getString(String key, @Nullable String defValue) {
              synchronized (mLock) {
                  awaitLoadedLocked();
                  String v = (String)mMap.get(key);
                  return v != null ? v : defValue;
              }
          }

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



          在同步方法內(nèi)調(diào)用了 wait() 方法,會(huì)一直等待 getSharedPreferences() 方法開(kāi)啟的線程讀取完數(shù)據(jù)才能繼續(xù)往下執(zhí)行,如果讀取幾 KB 的數(shù)據(jù)還好,假設(shè)讀取一個(gè)大的文件,勢(shì)必會(huì)造成主線程阻塞。

          2.MMKV的使用

          MMKV 是基于 mmap 內(nèi)存映射的 key-value 組件,底層序列化/反序列化使用 protobuf 實(shí)現(xiàn),性能高,穩(wěn)定性強(qiáng)。從 2015 年中至今在微信上使用,其性能和穩(wěn)定性經(jīng)過(guò)了時(shí)間的驗(yàn)證。近期也已移植到 Android / macOS / Win32 / POSIX 平臺(tái),一并開(kāi)源。


          MMKV優(yōu)點(diǎn)


          1.MMKV實(shí)現(xiàn)了SharedPreferences接口,可以無(wú)縫切換。


          2.通過(guò) mmap 內(nèi)存映射文件,提供一段可供隨時(shí)寫(xiě)入的內(nèi)存塊,App 只管往里面寫(xiě)數(shù)據(jù),由操作系統(tǒng)負(fù)責(zé)將內(nèi)存回寫(xiě)到文件,不必?fù)?dān)心
          crash 導(dǎo)致數(shù)據(jù)丟失。


          3.MMKV數(shù)據(jù)序列化方面選用 protobuf 協(xié)議,pb 在性能和空間占用上都有不錯(cuò)的表現(xiàn)。


          4.SP是全量更新,MMKV是增量更新,有性能優(yōu)勢(shì)。


          詳細(xì)的使用細(xì)節(jié)可以參考文檔:https://github.com/Tencent/MMKV/wiki

          3 MMKV原理

          IO操作


          我們知道,SP是寫(xiě)入是基于IO操作的,為了了解IO,我們需要先了解下用戶空間與內(nèi)核空間
          虛擬內(nèi)存被操作系統(tǒng)劃分成兩塊:用戶空間和內(nèi)核空間,用戶空間是用戶程序代碼運(yùn)行的地方,內(nèi)核空間是內(nèi)核代碼運(yùn)行的地方。為了安全,它們是隔離的,即使用戶的程序崩潰了,內(nèi)核也不受影響。 



          寫(xiě)文件流程:


          1、調(diào)用write,告訴內(nèi)核需要寫(xiě)入數(shù)據(jù)的開(kāi)始地址與長(zhǎng)度。


          2、內(nèi)核將數(shù)據(jù)拷貝到內(nèi)核緩存。


          3、由操作系統(tǒng)調(diào)用,將數(shù)據(jù)拷貝到磁盤(pán),完成寫(xiě)入。


          MMAP


          Linux通過(guò)將一個(gè)虛擬內(nèi)存區(qū)域與一個(gè)磁盤(pán)上的對(duì)象關(guān)聯(lián)起來(lái),以初始化這個(gè)虛擬內(nèi)存區(qū)域的內(nèi)容,這個(gè)過(guò)程稱為內(nèi)存映射(memory mapping)。



          對(duì)文件進(jìn)行mmap,會(huì)在進(jìn)程的虛擬內(nèi)存分配地址空間,創(chuàng)建映射關(guān)系。


          實(shí)現(xiàn)這樣的映射關(guān)系后,就可以采用指針的方式讀寫(xiě)操作這一段內(nèi)存,而系統(tǒng)會(huì)自動(dòng)回寫(xiě)到對(duì)應(yīng)的文件磁盤(pán)上


          MMAP優(yōu)勢(shì)


          1、MMAP對(duì)文件的讀寫(xiě)操作只需要從磁盤(pán)到用戶主存的一次數(shù)據(jù)拷貝過(guò)程,減少了數(shù)據(jù)的拷貝次數(shù),提高了文件讀寫(xiě)效率。


          2、MMAP使用邏輯內(nèi)存對(duì)磁盤(pán)文件進(jìn)行映射,操作內(nèi)存就相當(dāng)于操作文件,不需要開(kāi)啟線程,操作MMAP的速度和操作內(nèi)存的速度一樣快。


          3、MMAP提供一段可供隨時(shí)寫(xiě)入的內(nèi)存塊,App 只管往里面寫(xiě)數(shù)據(jù),由操作系統(tǒng)如內(nèi)存不足、進(jìn)程退出等時(shí)候負(fù)責(zé)將內(nèi)存回寫(xiě)到文件,不必?fù)?dān)心 crash 導(dǎo)致數(shù)據(jù)丟失。



          可以看出,MMAP的寫(xiě)入速度基本與內(nèi)存寫(xiě)入速度一致,遠(yuǎn)高于SP,這就是MMKV寫(xiě)入速度更快的原因。


          MMKV寫(xiě)入方式


          SP的數(shù)據(jù)結(jié)構(gòu)


          SP是使用XML格式存儲(chǔ)數(shù)據(jù)的,如下所示 。



          但是這也導(dǎo)致SP如果要更新數(shù)據(jù)的話,只能全量更新。


          MMKV數(shù)據(jù)結(jié)構(gòu)


          MMKV數(shù)據(jù)結(jié)構(gòu)如下



          MMKV使用Protobuf存儲(chǔ)數(shù)據(jù),冗余數(shù)據(jù)更少,更省空間,同時(shí)可以方便地在末尾追加數(shù)據(jù)。


          寫(xiě)入方式


          增量寫(xiě)入


          不管key是否重復(fù),直接將數(shù)據(jù)追加在前數(shù)據(jù)后。這樣效率更高,更新數(shù)據(jù)只需要插入一條數(shù)據(jù)即可。


          當(dāng)然這樣也會(huì)帶來(lái)問(wèn)題,如果不斷增量追加內(nèi)容,文件越來(lái)越大,怎么辦?


          當(dāng)文件大小不夠,這時(shí)候需要全量寫(xiě)入。將數(shù)據(jù)去掉重復(fù)key后,如果文件大小滿足寫(xiě)入的數(shù)據(jù)大小,則可以直接更新全量寫(xiě)入,否則需要擴(kuò)容。(在擴(kuò)容時(shí)根據(jù)平均每個(gè)K-V大小計(jì)算未來(lái)可能需要的文件大小進(jìn)行擴(kuò)容,防止經(jīng)常性的全量寫(xiě)入)


          MMKV三大優(yōu)勢(shì)


          • mmap防止數(shù)據(jù)丟失,提高讀寫(xiě)效率;


          • 精簡(jiǎn)數(shù)據(jù),以最少的數(shù)據(jù)量表示最多的信息,減少數(shù)據(jù)大小;


          • 增量更新,避免每次進(jìn)行相對(duì)增量來(lái)說(shuō)大數(shù)據(jù)量的全量寫(xiě)入。


          參考資料

          [Google] 再見(jiàn) SharedPreferences 擁抱 Jetpack DataStore

          https://juejin.cn/post/6881442312560803853

          淺析SharedPreferences

          https://juejin.cn/post/6844903729217404935


          中獎(jiǎng)名單

          請(qǐng)以下同學(xué)盡快聯(lián)系我,過(guò)期不候哇(截止時(shí)間:2021.4.30)


          Troll    甄可愛(ài)    心の力    Biubiu    逆水行舟

          跟妹子打王者又卡了!聊聊Android屏幕刷新機(jī)制(文末送書(shū))

          ·················END·················

          推薦閱讀

          ? 耗時(shí)2年,Android進(jìn)階三部曲第三部《Android進(jìn)階指北》出版!

          ? 『BATcoder』做了多年安卓還沒(méi)編譯過(guò)源碼?一個(gè)視頻帶你玩轉(zhuǎn)!

          ? 『BATcoder』是時(shí)候下載Android11系統(tǒng)源碼和內(nèi)核源碼了!

          ? 重生!進(jìn)階三部曲第一部《Android進(jìn)階之光》第2版 出版!

          BATcoder技術(shù)群,讓一部分人先進(jìn)大廠

          你好,我是劉望舒,騰訊云最具價(jià)值專家TVP,著有暢銷書(shū)《Android進(jìn)階之光》《Android進(jìn)階解密》《Android進(jìn)階指北》,蟬聯(lián)四屆電子工業(yè)出版社年度優(yōu)秀作者,谷歌開(kāi)發(fā)者社區(qū)特邀講師。

          前華為面試官,現(xiàn)大廠技術(shù)負(fù)責(zé)人。


          想要加入 BATcoder技術(shù)群,公號(hào)回復(fù)Android 即可。

          為了防止失聯(lián),歡迎關(guān)注我的小號(hào)


          更文不易,點(diǎn)個(gè)“在看”支持一下??
          瀏覽 62
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  成人福利视频 | av天堂影视 | 人妖A片 任你操逼 | 国产AV豆花看片 | 日本大道视频91 |