字節跳動:IO優化是怎麼做的,使用 SharedPreferences爲什麼這麼卡,mmkv原理是什麼

面試官: IO優化是怎麼做的,使用 SharedPreferences爲什麼這麼卡,mmkv原理是什麼

心理分析:IO優化一直是每個企業必選項,每次聞到都很頭疼,面試官想問有沒有相關經驗,如果有的話,只有兩種答案sqlitedatabse, SharedPreferences。 這兩個很常見,肯定不是面試官想問的。 那只有一種答案了,對,就是最新的mmkv框架

接下來,會問你他的原理 你是怎麼看。 它的優缺點。爲什麼比其他的好。從原理層來解析。這纔是最難的。

這篇文章 從原理層說明他們的區別

更多面試內容,面試專題,flutter視頻 全套,音視頻從0到高手開發。
關注GitHub:https://github.com/xiangjiana/Android-MS
免費獲取面試PDF合集

1.1 MMKV的概念

mkkv是基於 mmap 的高性能通用 key-value 組件,底層序列化/反序列化使用 protobuf 實現,性能高,穩定性強。

mmkv github下載地址

  1. MMKV 是基於 mmap 內存映射的移動端通用 key-value 組件,底層序列化/反序列化使用 protobuf 實現,性能高,穩定性強。
  2. 從 2015 年中至今,在Android 微信上使用已有近 3 年,其性能和穩定性經過了時間的驗證。在騰訊內部開源半年之後,得到公司內部團隊的廣泛應用和一致好評。
  3. 通過 mmap 內存映射文件,提供一段可供隨時寫入的內存塊,App 只管往裏面寫數據,
    由操作系統負責將內存回寫到文件,不必擔心 crash 導致數據丟失。
  4. XML、JSON 更注重數據結構化,關注人類可讀性和語義表達能力。 ProtoBuf 更注重數據序列化,關注效率、空間、速度,人類可讀性差,語義表達能力不足(爲保證極致的效率,會捨棄一部分元信息)

1.2 特點

  • 高性能 實時寫入
  • 穩定 防crash
  • 多進程訪問
    通過與 Android 開發同學的溝通,瞭解到系統自帶的 SharedPreferences 對多進程的支持不好。
    現有基於 ContentProvider 封裝的實現,雖然多進程是支持了,但是性能低下,經常導致 ANR。
    考慮到 mmap 共享內存本質上的多進程共享的,我們在這個基礎上,深入挖掘了 Android 系統的能力,提供了可能是業界最高效的多進程數據共享組件。
  • 匿名內存
    在多進程共享的基礎上,考慮到某些敏感數據(例如密碼)需要進程間共享,但是不方便落地存儲到文件上,直接用 mmap 不合適。
    我們瞭解到 Android 系統提供了 Ashmem 匿名共享內存的能力,發現它在進程退出後就會消失,不會落地到文件上,非常適合這個場景。
    我們很愉快地提供了 Ashmem MMKV 的功能。
  • 數據加密
    不像 iOS 提供了硬件層級的加密機制,在 Android 環境裏,數據加密是非常必須的。
    MMKV 使用了 AES CFB-128 算法來加密/解密。我們選擇 CFB 而不是常見的 CBC 算法,
    主要是因爲 MMKV 使用 append-only 實現插入/更新操作,流式加密算法更加合適。
  • 數據有效性

1.3性能對比

我們將 MMKV 和 SharedPreferences、SQLite 進行對比, 重複讀寫操作 1k 次。相關測試代碼在 Android/MMKV/mmkvdemo/。結果如下圖表。

單進程性能


可見,MMKV 在寫入性能上遠遠超越 SharedPreferences & SQLite,在讀取性能上也有相近或超越的表現。


可見,MMKV 無論是在寫入性能還是在讀取性能,都遠遠超越 MultiProcessSharedPreferences & SQLite & SQLite, MMKV 在 Android 多進程 key-value 存儲組件上是不二之選。

1.3 MMKV 原理

  • 內存準備 通過 mmap 內存映射文件,提供一段可供隨時寫入的內存塊,App 只管往裏面寫數據,由操作系統負責將內存回寫到文件,不必擔心 crash 導致數據丟失。

  • 數據組織 數據序列化方面我們選用 protobuf 協議,pb 在性能和空間佔用上都有不錯的表現。

  • 寫入優化 考慮到主要使用場景是頻繁地進行寫入更新,我們需要有增量更新的能力。我們考慮將增量 kv 對象序列化後,append 到內存末尾。 這樣同一個 key 會有新舊若干份數據,最新的數據在最後;那麼只需在程序啓動第一次打開 mmkv 時,不斷用後讀入的 value 替換之前的值,就可以保證數據是最新有效的。

  • 空間增長 使用 append 實現增量更新帶來了一個新的問題,就是不斷 append 的話,文件大小會增長得不可控。我們需要在性能和空間上做個折中。 以內存 pagesize 爲單位申請空間,在空間用盡之前都是 append 模式;當 append 到文件末尾時,進行文件重整、key 排重,嘗試序列化保存排重結果; 排重後空間還是不夠用的話,將文件擴大一倍,直到空間足夠。

  • 數據有效性 考慮到文件系統、操作系統都有一定的不穩定性,我們另外增加了 crc 校驗,對無效數據進行甄別。

更詳細的設計原理參考 MMKV 原理

1.4 快速上手

  dependencies {
      implementation 'com.tencent:mmkv:1.0.23'
      // replace "1.0.23" with any available version
  }

MMKV的使用非常簡單, 所有變更立馬生效,無需調用 sync、apply。 在 App 啓動時初始化 MMKV,設定 MMKV 的根目錄 (默認/data/data/xxx.xxx/files/mmkv/) (sp存儲在/data/data/xxx.xxx/shared_prefs/)

支持從SP遷移數據importFromSharedPreferences

MMKV 還額外實現了一遍 SharedPreferences、SharedPreferences.Editor 這兩個 interface

  // 可以跟SP用法一樣
  SharedPreferences.Editor editor = mmkv.edit();
  // 無需調用 commit()
  //editor.commit();

MMKV 的使用非常簡單,所有變更立馬生效,無需調用 sync、apply。 在 App 啓動時初始化 MMKV,設定 MMKV 的根目錄(files/mmkv/),例如在 MainActivity 裏:

  protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);

      String rootDir = MMKV.initialize(this);
      System.out.println("mmkv root: " + rootDir);
      //……
  }

MMKV 提供一個全局的實例,可以直接使用:

  import com.tencent.mmkv.MMKV;
  //……

  MMKV kv = MMKV.defaultMMKV();

  kv.encode("bool", true);
  boolean bValue = kv.decodeBool("bool");

  kv.encode("int", Integer.MIN_VALUE);
  int iValue = kv.decodeInt("int");

  kv.encode("string", "Hello from mmkv");
  String str = kv.decodeString("string");

使用完畢的幾個方法

      public native void clearAll();

      // MMKV's size won't reduce after deleting key-values
      // call this method after lots of deleting f you care about disk usage
      // note that `clearAll` has the similar effect of `trim`
      public native void trim();

      // call this method if the instance is no longer needed in the near future
      // any subsequent call to the instance is undefined behavior
      public native void close();

      // call on memory warning
      // any subsequent call to the instance will load all key-values from file again
      public native void clearMemoryCache();

      // you don't need to call this, really, I mean it
      // unless you care about out of battery
      public void sync() {
          sync(true);
      }

1.5 補充適用建議

如果使用請務必做code19版本的適配,這個在github官網有說明

依賴下面這個庫,然後對19區分處理
implementation ‘com.getkeepsafe.relinker:relinker:1.3.1’

  if (android.os.Build.VERSION.SDK_INT == 19) {
      MMKV.initialize(relativePath, new MMKV.LibLoader() {
          @Override
          public void loadLibrary(String libName) {
              ReLinker.loadLibrary(context, libName);
          }
      });
  } else {
      MMKV.initialize(context);
  }

1.6 限制

可看到,一個鍵會存入多分實例,最後存入的就是最新的。
MMKV 在大部分情況下都性能強勁,key/value 的數量和長度都沒有限制。
然而 MMKV 在內存裏緩存了所有的 key-value,在總大小比較大的情況下(例如 100M+),App 可能會爆內存,觸發重整回寫時,寫入速度也會變慢。
支持大文件的 MMKV 正在開發中,有望在下一個大版本發佈。

1.7 多進程使用

1.7.1鎖 lock unlock tryLock

注意如果一個進程lock住,另一個進程mmkvWithID獲取MMKV時就阻塞住,直到持有進程釋放。

          // get the lock immediately
        MMKV mmkv2 = MMKV.mmkvWithID(LOCK_PHASE_2, MMKV.MULTI_PROCESS_MODE);
        mmkv2.lock();
        Log.d("locked in child", LOCK_PHASE_2);

        Runnable waiter = new Runnable() {
            @Override
            public void run() {
                //阻塞住 直到其他進程釋放
                MMKV mmkv1 = MMKV.mmkvWithID(LOCK_PHASE_1, MMKV.MULTI_PROCESS_MODE);
                mmkv1.lock();
                Log.d("locked in child", LOCK_PHASE_1);
            }
        };

​ 注意:如果其他進程有進行修改,不會立即觸發onContentChangedByOuterProcess,
checkLoadData如果變化,會clearMemoryState,重新loadFromFile。//數據量大時不要太頻繁
讀取decodeXXX會阻塞住,先回調onContentChangedByOuterProcess,再返回值,保證值是最新的。

1.7.2 mmkvWithAshmemID 匿名共享內存

可以進行進程間通信,可設置pageSize
// a memory only MMKV, cleared on program exit
// size cannot change afterward (because ashmem won't allow it)

1.7.3 測試結果

write速度: mmkv > cryptKV >> sp
read速度: sp > cryptKV > mmkv

1.8 Binder MMAP(一次拷貝)

​ Linux的內存分用戶空間跟內核空間,同時頁表有也分兩類,用戶空間頁表跟內核空間頁表,每個進程有一個用戶空間頁表,但是系統只有一個內核空間頁表。
而Binder mmap的關鍵是:更新用戶空間對應的頁表的同時也同步映射內核頁表,讓兩個頁表都指向同一塊地址,
​ 這樣一來,數據只需要從A進程的用戶空間,直接拷貝到B所對應的內核空間,而B多對應的內核空間在B進程的用戶空間也有相應的映射,這樣就無需從內核拷貝到用戶空間了。

copy_from_user() //將數據從用戶空間拷貝到內核空間
copy_to_user() //將數據從內核空間拷貝到用戶空間

1.8.1 Liunx進程隔離


1.8.2 傳統IPC


1.8.3 Binder通信

 

1.9 普通文件mmap原理

普通文件的訪問方式有兩種

  1. ​ 第一種是通過read/write系統調訪問,先在用戶空間分配一段buffer,然後,進入內核,將內容從磁盤讀 取到內核緩衝,最後,拷貝到用戶進程空間,至少牽扯到兩次數據拷貝;
    同時,多個進程同時訪問一個文件,每個進程都有一個副本,存在資源浪費的問題。
  2. ​ 另一種是通過mmap來訪問文件,mmap()將文件直接映射到用戶空間,文件在mmap的時候,內存並未 真正分配,

​ 只有在第一次讀取/寫入的時候纔會觸發,這個時候,會引發缺頁中斷,在處理缺頁中斷的時候,完成內存也分配,同時也完成文件數據的拷貝。
並且,修改用戶空間對應的頁表,完成到物理內存到用戶空間的映射,這種方式只存在一次數據拷貝,效率更高。

同時多進程間通過mmap共享文件數據的時候,僅需要一塊物理內存就夠了。

​ Android中使用mmap,可以通過RandomAccessFile與MappedByteBuffer來配合。
通過randomAccessFile.getChannel().map獲取到MappedByteBuffer。然後調用ByteBuffer的put方法添加數據。

  RandomAccessFile randomAccessFile = new RandomAccessFile("path","rw");
  MappedByteBuffer mappedByteBuffer= 
  randomAccessFile.getChannel().map(FileChannel.MapMode.READ_WRITE,0, 
  mappedByteBuffer.putChar('c');
  mappedByteBuffer.getChar();

更多面試內容,面試專題,flutter視頻 全套,音視頻從0到高手開發。
關注GitHub:https://github.com/xiangjiana/Android-MS
免費獲取面試PDF合集

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章