SharedPreference與mmkv

一、持久化數(shù)據(jù)Key-Values存儲方案颓芭,如何設(shè)計亡问?

1州藕、新建磁盤文件:涉及IO讀寫 (效率問題)
2床玻、選取數(shù)據(jù)格式:xml锈死,json待牵,protocol (增刪改查問題)
3喇勋、映射到內(nèi)存:map集合(內(nèi)存占用問題)
4缨该、提供get put 方法,修改內(nèi)存茄蚯,修改文件压彭,(數(shù)據(jù)一致性問題)

SharedPreference原理

1睦优、初始化

  • 1.1、新建子線程壮不,使用傳統(tǒng)IO汗盘,讀取xml格式keyVelues,映射成map集合
    SharedPreferencesImpl 源碼
private void loadFromDisk() {
    synchronized (mLock) {
        if (mLoaded) {
            return;
        }
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
   ...
}
  • 1.2询一、get方法
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
        awaitLoadedLocked();
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}

問題:如果沒有初始化完成缩功,出現(xiàn)線程阻塞問題

  • 1.3、put方法
public void apply() {
    final long startTime = System.currentTimeMillis();

    // 1啦桌、先更新內(nèi)存數(shù)據(jù)
    final MemoryCommitResult mcr = commitToMemory();
    // 2钾虐、更新磁盤數(shù)據(jù)
    final Runnable awaitCommit = new Runnable() {
            @Override
            public void run() {
                try {
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }

                if (DEBUG && mcr.wasWritten) {
                    Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                            + " applied after " + (System.currentTimeMillis() - startTime)
                            + " ms");
                }
            }
        };
        ....
        }
@GuardedBy("mWritingToDiskLock")
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
   ...
    try {
        FileOutputStream str = createFileOutputStream(mFile);
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
                ...
        try {
            final StructStat stat = Os.stat(mFile.getPath());
            synchronized (mLock) {
                mStatTimestamp = stat.st_mtim;
                mStatSize = stat.st_size;
            }
        } catch (ErrnoException e) {
            // Do nothing
        }
                ...
        return;
    } catch (XmlPullParserException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    } catch (IOException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    }
    
}

問題:

1、寫磁盤存在失敗情況摹迷,出現(xiàn)磁盤數(shù)據(jù)和內(nèi)存數(shù)據(jù)不一致的情況
2、出現(xiàn)ANR風(fēng)險
anr原因分析:
QueuedWork.addFinisher(awaitCommit);
QueuedWork.waitToFinish();
查看QueuedWork 以及 ActivityThread 源碼

總結(jié):
SharedPreference的問題

SharedPreference 問題 解決方案
IO讀寫 傳統(tǒng)IO讀寫熙掺,效率慢 类浪?
數(shù)據(jù)格式xml 不支持局部更新 力细?
內(nèi)存映射 餓漢模式昔脯,初始化就創(chuàng)建map
可靠性 存在數(shù)據(jù)不一致性拐格,存在ANR風(fēng)險
多進程 不支持多進程數(shù)據(jù)共享 ?

二姐军、文件拷貝相關(guān)知識補充

應(yīng)用運行時,存在用戶空間內(nèi)存辽话,和內(nèi)核空間,兩個空間的內(nèi)存是隔離的,互相不影響帘瞭。下圖所示:


image.png

應(yīng)用層的任何代碼執(zhí)行都是在用戶空間執(zhí)行的全封,如果要進行系統(tǒng)調(diào)用行楞,比如IO操作,那么就需要進入內(nèi)核空間,在內(nèi)核空間實現(xiàn)系統(tǒng)調(diào)用乎莉。

那么,用戶空間和內(nèi)核空間如何實現(xiàn)數(shù)據(jù)共享呢券册?答案就是通過CPU拷貝來實現(xiàn)的赚窃。

下圖顯示了,一次傳統(tǒng)IO操作的過程發(fā)生的內(nèi)存拷貝操作:

    File source = new File("/Users/dw/applogs.zip");
        File dest = new File("/Users/dw/applogs_io.zip");
        java.io.InputStream input = null;
        java.io.OutputStream output = null;
         
        input = new FileInputStream(source);
        output = new FileOutputStream(dest);
        byte[] buf = new byte[1024];
        int bytes;
        while ((bytes = input.read(buf)) > 0) {
             output.write(buf, 0, bytes);
        }
image.png

1岔激、read系統(tǒng)調(diào)用考榨,DMA執(zhí)行了一次數(shù)據(jù)拷貝,從磁盤拷貝到內(nèi)核空間
2鹦倚、read結(jié)束后河质,發(fā)生了第二次數(shù)據(jù)拷貝,由CPU將數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間
3震叙、write系統(tǒng)調(diào)用掀鹅,CPU將用戶空間的數(shù)據(jù)拷貝到內(nèi)核空間
4、write結(jié)束媒楼,DMA執(zhí)行了一次數(shù)據(jù)拷貝乐尊,從磁盤拷貝到內(nèi)核空間

以上過程,發(fā)生了4次數(shù)據(jù)拷貝划址,兩次上下文切換扔嵌。

  • DMA(Direct Memory Access,直接[存儲器])夺颤,DMA 傳輸將數(shù)據(jù)從一個地址空間復(fù)制到另外一個地址空間痢缎。當(dāng)CPU 初始化這個傳輸動作,傳輸動作本身是由 DMA 控制器來實行和完成世澜。

三独旷、零拷貝技術(shù)

  • 零拷貝,不是真正的不拷貝寥裂,而是減少CPU的拷貝操作嵌洼,避免了CPU將數(shù)據(jù)從一塊內(nèi)存區(qū)拷貝到另一塊內(nèi)存區(qū),用戶程序繞過操作系統(tǒng)封恰,直接操作系統(tǒng)資源麻养,數(shù)據(jù)傳輸使用DMA傳輸,讓數(shù)據(jù)拷貝不需要經(jīng)過用戶空間诺舔。

  • 操作內(nèi)存鳖昌,就等于操作磁盤

  • java自帶的零拷貝實現(xiàn)類:

    java.nio.channels.FileChannel 和 java.io.InputStream 拷貝文件效率對比演示

四备畦、mmap

  • mmap操作提供了一種機制,讓用戶程序直接訪問設(shè)備內(nèi)存遗遵,這種機制萍恕,相比較在用戶空間和內(nèi)核空間互相拷貝數(shù)據(jù)逸嘀,效率更高。在要求高性能的應(yīng)用中比較常用。mmap映射內(nèi)存必須是頁面大小的整數(shù)倍弊予,面向流的設(shè)備不能進行mmap悦析,mmap的實現(xiàn)和硬件有關(guān)。
  • mmap是一種內(nèi)存映射文件的方法司光,即將一個文件或者其它對象映射到進程的地址空間琅坡,實現(xiàn)文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對映關(guān)系。實現(xiàn)這樣的映射關(guān)系后残家,進程就可以采用指針的方式讀寫操作這一段內(nèi)存榆俺,而系統(tǒng)會自動回寫臟頁面到對應(yīng)的文件磁盤上,即完成了對文件的操作而不必再調(diào)用read,write等系統(tǒng)調(diào)用函數(shù)坞淮。相反茴晋,內(nèi)核空間對這段區(qū)域的修改也直接反映用戶空間,從而可以實現(xiàn)不同進程間的文件共享回窘。如下圖所示:


    image.png

4.1 mmap的write

1.進程(用戶態(tài))將需要寫入的數(shù)據(jù)直接copy到對應(yīng)的mmap地址(內(nèi)存copy)
2.若mmap地址未對應(yīng)物理內(nèi)存诺擅,則產(chǎn)生缺頁異常,由內(nèi)核處理
3.若已對應(yīng)啡直,則直接copy到對應(yīng)的物理內(nèi)存
4.由操作系統(tǒng)調(diào)用烁涌,將臟頁回寫到磁盤(通常是異步的)

4.2 mmap的read

image.png

4.3 mmap函數(shù)

mmap函數(shù)
映射文件

 void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);

解除映射

int munmap(void *addr, size_t length);

函數(shù)參數(shù)解析

4.3 mmap總結(jié)

優(yōu)點:

  • 1、用內(nèi)存讀寫取代I/O讀寫酒觅,不需要開啟子線程撮执,操作mmap的速度和操作內(nèi)存速度一樣快,提高了文件讀取效率舷丹。
  • 2二打、用戶空間和內(nèi)核空間高效數(shù)據(jù)通訊方式
  • 3、多進程可以映射同一個文件掂榔,可以實現(xiàn)文件共享
  • 4继效、內(nèi)存不足或者應(yīng)用程序意外崩潰,系統(tǒng)會負(fù)責(zé)臟數(shù)據(jù)回寫到磁盤装获,不擔(dān)心數(shù)據(jù)丟失

缺點:

  • 1瑞信、內(nèi)存映射必須以頁(4096字節(jié))為單位進行映射,如果一個文件大小不足4k穴豫,那么映射整個頁4096字節(jié)映射凡简,造成一些內(nèi)存浪費

mmap讀寫數(shù)據(jù)速度


image.png

五逼友、MMKV實現(xiàn)

mmkv 是騰訊開源的使用mmap原理實現(xiàn)的高效的Key-Values存儲方案,在2015年時候開始在微信使用秤涩。

mmkv效率演示

 
    private fun spTest() {
        val start = System.currentTimeMillis()
        val sharedPreferences = this.getSharedPreferences("sp_name", Context.MODE_PRIVATE)
        val edit = sharedPreferences.edit()
        for (i in 0..3000) {
            edit.putInt("key$i", i).apply()
        }
        Log.i(TAG, "spTest cost ${System.currentTimeMillis() - start} ms")
    }

    private fun mmkvTest() {
        val start = System.currentTimeMillis()
        val defaultMMKV = MMKV.defaultMMKV()
        for (i in 0..3000) {
            defaultMMKV.putInt("key$i", i).apply()
        }
        Log.i(TAG, "mmkvTest cost ${System.currentTimeMillis() - start} ms")
    }

// 調(diào)用方
 Thread {
            mmkvTest()
            spTest()
        }.start()
  

2021-09-25 16:26:25.680 18488-18564/com.douwan.launchdemo I/MainActivity: mmkvTest cost 52 ms
2021-09-25 16:26:27.599 18488-18564/com.douwan.launchdemo I/MainActivity: spTest cost 1919 ms

5.1帜乞、mmkv數(shù)據(jù)結(jié)構(gòu)

數(shù)據(jù)格式使用 protobuf 協(xié)議,數(shù)據(jù)結(jié)構(gòu)更加精簡筐眷,key類型是String黎烈,values類型統(tǒng)一系列化為buffer。

message KV {
    string key = 1;
    buffer value = 2;
}

mmkv選擇的數(shù)據(jù)結(jié)構(gòu)是Key-Values鏈表結(jié)構(gòu)


image.png

5.2 mmkv寫入方式

  • 寫入優(yōu)化
    protobuf沒有提供增量更新能力匀谣,每次都是全量寫入照棋,所以,需要設(shè)計符合增量更新的能力武翎。將kv對象系列化之后烈炭,直接append到內(nèi)存末尾,同一個key可能會存在多份數(shù)據(jù)宝恶,最新的數(shù)據(jù)在最后符隙,這樣,只需要在應(yīng)用啟動后第一次打開mmkv垫毙,不斷用后讀取的values替換前面的值霹疫,就保證了數(shù)據(jù)是最新有效的。

  • 空間增長
    append帶來的一個問題就是文件大小會不斷的增長露久,變得不可控更米,所以解決方案是,當(dāng)文件空間用盡前毫痕,都是append模式征峦,append到文件末尾時候,進行文件重整消请,key排重栏笆,排重之后,如果文件空間還是不夠用的話臊泰,將文件擴大一倍蛉加,直到空間夠用。

5.3 mmkv

官方文檔

映射完成之后缸逃,直接操作內(nèi)存中的map集合针饥,完成數(shù)據(jù)讀寫操作。
對這片映射空間進行了讀寫操作需频,會引發(fā)缺頁異常丁眼,系統(tǒng)會自動回寫臟頁面到對應(yīng)的文件磁盤上,實現(xiàn)數(shù)據(jù)持久化存儲

//get方法
float MMKV::getFloat(MMKVKey_t key, float defaultValue) {
    if (isKeyEmpty(key)) {
        return defaultValue;
    }
    SCOPED_LOCK(m_lock);
    auto data = getDataForKey(key);
    if (data.length() > 0) {
        try {
            CodedInputData input(data.getPtr(), data.length());
            return input.readFloat();
        } catch (std::exception &exception) {
            MMKVError("%s", exception.what());
        }
    }
    return defaultValue;
}


MMBuffer MMKV::getDataForKey(MMKVKey_t key) {
    checkLoadData();
#ifndef MMKV_DISABLE_CRYPT
    if (m_crypter) { // 有加密
        auto itr = m_dicCrypt->find(key);
        if (itr != m_dicCrypt->end()) {
            auto basePtr = (uint8_t *) (m_file->getMemory()) + Fixed32Size;
            return itr->second.toMMBuffer(basePtr, m_crypter);
        }
    } else
#endif
    { //未加密
        auto itr = m_dic->find(key);
        if (itr != m_dic->end()) {
            auto basePtr = (uint8_t *) (m_file->getMemory()) + Fixed32Size;
            return itr->second.toMMBuffer(basePtr);
        }
    }
    MMBuffer nan;
    return nan;
}

// m_dicCrypt 昭殉, m_dic 定義:
    mmkv::MMKVMap *m_dic;
    mmkv::MMKVMapCrypt *m_dicCrypt;

// MMKVMap , MMKVMapCrypt 結(jié)構(gòu)體苞七,
// unordered_map: C++定義的map藐守,內(nèi)部實現(xiàn)了哈希表,其查找速度非常的快
using MMKVMap = std::unordered_map<NSString *, mmkv::KeyValueHolder, KeyHasher, KeyEqualer>;
using MMKVMapCrypt = std::unordered_map<NSString *, mmkv::KeyValueHolderCrypt, KeyHasher, KeyEqualer>;

六蹂风、MMKV vs SharedPreference

SharedPreference 問題 MMKV
IO讀寫 傳統(tǒng)IO讀寫卢厂,效率慢 mmap方式讀寫磁盤
數(shù)據(jù)格式xml 不支持局部更新 protocol結(jié)構(gòu)體 Key-Values鏈表方式
內(nèi)存映射 餓漢模式,初始化就創(chuàng)建map 初始化只是映射一個內(nèi)存地址
可靠性 存在數(shù)據(jù)不一致性惠啄,存在ANR風(fēng)險 直接操作內(nèi)存慎恒,不存在數(shù)據(jù)一致性問題
多進程 不支持多進程數(shù)據(jù)共享 支持多進程數(shù)據(jù)共享
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市礁阁,隨后出現(xiàn)的幾起案子巧号,更是在濱河造成了極大的恐慌族奢,老刑警劉巖姥闭,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異越走,居然都是意外死亡棚品,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門廊敌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來铜跑,“玉大人,你說我怎么就攤上這事骡澈」模” “怎么了?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵肋殴,是天一觀的道長囤锉。 經(jīng)常有香客問我,道長护锤,這世上最難降的妖魔是什么官地? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮烙懦,結(jié)果婚禮上驱入,老公的妹妹穿的比我還像新娘。我一直安慰自己氯析,他們只是感情好亏较,可當(dāng)我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著掩缓,像睡著了一般雪情。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上拾因,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天旺罢,我揣著相機與錄音旷余,去河邊找鬼。 笑死扁达,一個胖子當(dāng)著我的面吹牛正卧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播跪解,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼炉旷,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了叉讥?” 一聲冷哼從身側(cè)響起窘行,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎图仓,沒想到半個月后罐盔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡救崔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年惶看,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片六孵。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡纬黎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出劫窒,到底是詐尸還是另有隱情,我是刑警寧澤冠息,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布瓮孙,位于F島的核電站杭抠,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏翁垂。R本人自食惡果不足惜沿猜,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一祈坠、第九天 我趴在偏房一處隱蔽的房頂上張望慌随。 院中可真熱鬧花嘶,春花似錦车海、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽致板。三九已至斟或,卻和暖如春萝挤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工瘦馍, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留歼秽,地道東北人。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓情组,卻偏偏與公主長得像燥筷,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子院崇,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容

  • SharedPreference 數(shù)據(jù)格式 XML格式保存肆氓,使用Pull解析 初始化 創(chuàng)建SharedPrefer...
    Archer_J閱讀 1,395評論 2 2
  • MMKV 簡介 MMKV——基于 mmap 的高性能通用 key-value 組件MMKV 是基于 mmap 內(nèi)存...
    who_young閱讀 1,916評論 0 2
  • 前言 好久沒有更新常用的第三方庫了。讓我們來聊聊MMKV這個常用的第三方庫底瓣。MMKV這個庫是做什么的呢谢揪?他本質(zhì)上的...
    yjy239閱讀 11,042評論 7 14
  • 1 MMKV概覽 1.1 什么是MMKV 引自 github.com/Tencent/MMKV[http://gi...
    蔣斌文閱讀 5,984評論 0 1
  • 16宿命:用概率思維提高你的勝算 以前的我是風(fēng)險厭惡者,不喜歡去冒險捐凭,但是人生放棄了冒險拨扶,也就放棄了無數(shù)的可能。 ...
    yichen大刀閱讀 6,033評論 0 4