MMKV for Android 多進(jìn)程設(shè)計(jì)與實(shí)現(xiàn)(二)

IPC 選型

說到 IPC贩据,首要的問題就是架構(gòu)選型官帘,不同的架構(gòu)效果大相徑庭。

CS 架構(gòu) vs 去中心化架構(gòu)

Android 平臺(tái)第一個(gè)想到的就是 ContentProvider:一個(gè)單獨(dú)進(jìn)程管理數(shù)據(jù),數(shù)據(jù)同步不易出錯(cuò)家淤,簡單好用易上手。然而它的問題也很明顯愚屁,就是一個(gè)字:啟動(dòng)慢济竹,訪問也慢。這個(gè)可以說是 Android 下基于 Binder 的 CS 架構(gòu)組件的通用痛點(diǎn)霎槐。至于其他的 CS 架構(gòu)送浊,例如經(jīng)典的 socket、PIPE栽燕、message queue罕袋,因?yàn)橐辽?2 次的內(nèi)存拷貝,就更加慢了碍岔。

MMKV 追求的是極致的訪問速度浴讯,我們要盡可能地避免進(jìn)程間通信,CS 架構(gòu)是不可取的蔼啦。再考慮到 MMKV 底層使用 mmap 實(shí)現(xiàn)榆纽,采用去中心化的架構(gòu)是很自然的選擇。我們只需要將文件 mmap 到每個(gè)訪問進(jìn)程的內(nèi)存空間捏肢,加上合適的進(jìn)程鎖奈籽,再處理好數(shù)據(jù)的同步,就能夠?qū)崿F(xiàn)多進(jìn)程并發(fā)訪問鸵赫。

挑選進(jìn)程鎖

然而去中心化的架構(gòu)實(shí)現(xiàn)起來并不簡單衣屏,Android 是個(gè)閹割版的 Linux,IPC 組件的支持比較殘缺辩棒。例如狼忱,說到進(jìn)程鎖第一個(gè)想到的就是 pthread 庫的 pthread_mutex,創(chuàng)建于共享內(nèi)存的 pthread_mutex 是可以用作進(jìn)程鎖的一睁,然而 Android 版的 pthread_mutex 并不保證robust钻弄,亦即對(duì) pthread_mutex 加了鎖的進(jìn)程被 kill,系統(tǒng)不會(huì)進(jìn)行清理工作者吁,這個(gè)鎖會(huì)一直存在下去窘俺,那么其他等鎖的進(jìn)程就會(huì)永遠(yuǎn)餓死。其他的 IPC 組件复凳,例如信號(hào)量瘤泪、條件變量,也有同樣問題育八,Android 為了能夠盡快關(guān)閉進(jìn)程均芽,真是無所不用其極。

找了一圈单鹿,能夠保證 robust 的掀宋,只有已打開的文件描述符,以及基于文件描述符的文件鎖和 Binder 組件的死亡通知(是的,Binder 也是依賴這個(gè)清理機(jī)制運(yùn)作劲妙,打開的文件是 /dev/binder)湃鹊。

我們有兩個(gè)選擇:

  • 文件鎖,優(yōu)點(diǎn)是天然 robust镣奋,缺點(diǎn)是不支持遞歸加鎖币呵,也不支持讀寫鎖升級(jí)/降級(jí),需要自行實(shí)現(xiàn)侨颈。
  • pthread_mutex余赢,優(yōu)點(diǎn)是 pthread 庫支持遞歸加鎖,也支持讀寫鎖升級(jí)/降級(jí)哈垢,缺點(diǎn)是不 robust妻柒,需要自行清理。

關(guān)于 mutex 清理耘分,有個(gè)可能的方案是基于 Binder 死亡通知進(jìn)行清理:A举塔、B進(jìn)程相互注冊對(duì)方的死亡通知,在對(duì)方死亡的時(shí)候進(jìn)行清理求泰。但有個(gè)比較棘手的場景:只有 A 進(jìn)程存在央渣,那么他的死亡通知就沒人處理,留下一個(gè)永遠(yuǎn)加鎖的 mutex渴频。Binder 規(guī)定死亡通知不能本進(jìn)程自行處理芽丹,必須由其他進(jìn)程處理,所以這個(gè)問題不好解決卜朗。

綜合各種考慮志衍,我們先將文件鎖作為一個(gè)簡單的互斥鎖,進(jìn)行 MMKV 的多進(jìn)程開發(fā)聊替,稍后再回頭解決遞歸鎖和讀寫鎖升級(jí)/降級(jí)的問題。

多進(jìn)程實(shí)現(xiàn)細(xì)節(jié)

首先我們簡單回顧一下 MMKV 原來的邏輯培廓。MMKV 本質(zhì)上是將文件 mmap 到內(nèi)存塊中惹悄,將新增的 key-value 統(tǒng)統(tǒng) append 到內(nèi)存中;到達(dá)邊界后肩钠,進(jìn)行重整回寫以騰出空間泣港,空間還是不夠的話,就 double 內(nèi)存空間价匠;對(duì)于內(nèi)存文件中可能存在的重復(fù)鍵值当纱,MMKV 只選用最后寫入的作為有效鍵值。那么其他進(jìn)程為了保持?jǐn)?shù)據(jù)一致踩窖,就需要處理這三種情況:寫指針增長坡氯、內(nèi)存重整、內(nèi)存增長。但首先還得解決一個(gè)問題:怎么讓其他進(jìn)程感知這三種情況箫柳?

狀態(tài)同步

  • 寫指針的同步
    我們可以在每個(gè)進(jìn)程內(nèi)部緩存自己的寫指針手形,然后在寫入鍵值的同時(shí),還要把最新的寫指針位置也寫到 mmap 內(nèi)存中悯恍;這樣每個(gè)進(jìn)程只需要對(duì)比一下緩存的指針與 mmap 內(nèi)存的寫指針库糠,如果不一樣,就說明其他進(jìn)程進(jìn)行了寫操作涮毫。事實(shí)上 MMKV 原本就在文件頭部保存了有效內(nèi)存的大小瞬欧,這個(gè)數(shù)值剛好就是寫指針的內(nèi)存偏移量,我們可以重用這個(gè)數(shù)值來校對(duì)寫指針罢防。

  • 內(nèi)存重整的感知
    考慮使用一個(gè)單調(diào)遞增的序列號(hào)艘虎,每次發(fā)生內(nèi)存重整,就將序列號(hào)遞增篙梢。將這個(gè)序列號(hào)也放到 mmap 內(nèi)存中顷帖,每個(gè)進(jìn)程內(nèi)部也緩存一份,只需要對(duì)比序列號(hào)是否一致渤滞,就能夠知道其他進(jìn)程是否觸發(fā)了內(nèi)存重整贬墩。

  • 內(nèi)存增長的感知
    事實(shí)上 MMKV 在內(nèi)存增長之前,會(huì)先嘗試通過內(nèi)存重整來騰出空間妄呕,重整后還不夠空間才申請(qǐng)新的內(nèi)存陶舞。所以內(nèi)存增長可以跟內(nèi)存重整一樣處理。至于新的內(nèi)存大小绪励,可以通過查詢文件大小來獲得肿孵,無需在 mmap 內(nèi)存另外存放。

狀態(tài)同步邏輯用偽碼表達(dá)大概是這個(gè)樣子:

void checkLoadData() {
    if (m_sequence != mmapSequence()) {
        m_sequence = mmapSequence();
        if (m_size != fileSize()) {
            m_size = fileSize();
            // 處理內(nèi)存增長
        } else {
            // 處理內(nèi)存重整
        }
    } else if (m_actualSize != mmapActualSize()) {
        auto lastPosition = m_actualSize;
        m_actualSize = mmapActualSize();
        // 處理寫指針增長
    } else {
        // 什么也沒發(fā)生
        return;
    }
}

寫指針增長

當(dāng)一個(gè)進(jìn)程發(fā)現(xiàn) mmap 寫指針增長疏魏,就意味著其他進(jìn)程寫入了新鍵值停做。這些新的鍵值都 append 在原有寫指針后面,可能跟前面的 key 重復(fù)大莫,也可能是全新的 key蛉腌,而原寫指針前面的鍵值都是有效的。那么我們就要把這些新鍵值都讀出來只厘,插入或替換原有鍵值烙丛,并將寫指針同步到最新位置。

    auto lastPosition = m_actualSize;
    m_actualSize = mmapActualSize();
    // 處理寫指針增長
    auto bufferSize = m_actualSize - lastPosition;
    auto buffer = Buffer(lastPosition, bufferSize);
    map<string, Buffer> dictionary = decodeMap(buffer);
    for (auto& itr : dictionary) {
        // m_cache 還是有效的
        m_cache[itr.first] = itr.second;
    }

內(nèi)存重整

當(dāng)一個(gè)進(jìn)程發(fā)現(xiàn)內(nèi)存被重整了羔味,就意味著原寫指針前面的鍵值全部失效河咽,那么最簡單的做法是全部拋棄掉,從頭開始重新加載一遍赋元。

    // 處理內(nèi)存重整
    m_actualSize = mmapActualSize();
    auto buffer = Buffer(0, m_actualSize);
    m_cache = decodeMap(buffer);

內(nèi)存增長

正如前文所述忘蟹,發(fā)生內(nèi)存增長的時(shí)候飒房,必然已經(jīng)先發(fā)生了內(nèi)存重整,那么原寫指針前面的鍵值也是統(tǒng)統(tǒng)失效寒瓦,處理邏輯跟內(nèi)存重整一樣情屹。

文件鎖

到這里我們已經(jīng)完成了數(shù)據(jù)的多進(jìn)程同步工作,是時(shí)候回頭處理鎖事了杂腰,亦即前面提到的遞歸鎖和鎖升級(jí)/降級(jí)垃你。

  • 遞歸鎖
    意思是如果一個(gè)進(jìn)程/線程已經(jīng)擁有了鎖,那么后續(xù)的加鎖操作不會(huì)導(dǎo)致卡死喂很,并且解鎖也不會(huì)導(dǎo)致外層的鎖被解掉惜颇。對(duì)于文件鎖來說,前者是滿足的少辣,后者則不然凌摄。因?yàn)?strong>文件鎖是狀態(tài)鎖,沒有計(jì)數(shù)器漓帅,無論加了多少次鎖锨亏,一個(gè)解鎖操作就全解掉。只要用到子函數(shù)忙干,就非常需要遞歸鎖器予。

  • 鎖升級(jí)/降級(jí)
    鎖升級(jí)是指將已經(jīng)持有的共享鎖,升級(jí)為互斥鎖捐迫,亦即將讀鎖升級(jí)為寫鎖乾翔;鎖降級(jí)則是反過來。文件鎖支持鎖升級(jí)施戴,但是容易死鎖:假如 A反浓、B 進(jìn)程都持有了讀鎖,現(xiàn)在都想升級(jí)到寫鎖赞哗,就會(huì)陷入相互等待的困境雷则,發(fā)生死鎖。另外肪笋,由于文件鎖不支持遞歸鎖月劈,也導(dǎo)致了鎖降級(jí)無法進(jìn)行,一降就降到?jīng)]有鎖涂乌。

為了解決這兩個(gè)難題,需要對(duì)文件鎖進(jìn)行封裝英岭,增加讀鎖湾盒、寫鎖計(jì)數(shù)器。處理邏輯如下表:

讀鎖計(jì)數(shù)器 寫鎖計(jì)數(shù)器 加讀鎖 加寫鎖 解讀鎖 解寫鎖
0 0 加讀鎖 加寫鎖 - -
0 1 +1 +1 - 解寫鎖
0 N +1 +1 - -1
1 0 +1 解讀鎖再加寫鎖 解讀鎖 -
1 1 +1 +1 -1 加讀鎖
1 N +1 +1 -1 -1
N 0 +1 解讀鎖再加寫鎖 -1 -
N 1 +1 +1 -1 加讀鎖
N N +1 +1 -1 -1

需要注意的地方有兩點(diǎn):

  • 加寫鎖時(shí)诅妹,如果當(dāng)前已經(jīng)持有讀鎖罚勾,那么先嘗試加寫鎖毅人,try_lock 失敗說明其他進(jìn)程持有了讀鎖,我們需要先將自己的讀鎖釋放掉尖殃,再進(jìn)行加寫鎖操作丈莺,以避免死鎖的發(fā)生。
  • 解寫鎖時(shí)送丰,假如之前曾經(jīng)持有讀鎖缔俄,那么我們不能直接釋放掉寫鎖,這樣會(huì)導(dǎo)致讀鎖也解了器躏。我們應(yīng)該加一個(gè)讀鎖俐载,將鎖降級(jí)。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末登失,一起剝皮案震驚了整個(gè)濱河市遏佣,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌揽浙,老刑警劉巖状婶,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異馅巷,居然都是意外死亡膛虫,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門令杈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來走敌,“玉大人,你說我怎么就攤上這事逗噩〉衾觯” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵异雁,是天一觀的道長捶障。 經(jīng)常有香客問我,道長纲刀,這世上最難降的妖魔是什么项炼? 我笑而不...
    開封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮示绊,結(jié)果婚禮上锭部,老公的妹妹穿的比我還像新娘。我一直安慰自己面褐,他們只是感情好拌禾,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著展哭,像睡著了一般湃窍。 火紅的嫁衣襯著肌膚如雪闻蛀。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天您市,我揣著相機(jī)與錄音觉痛,去河邊找鬼。 笑死茵休,一個(gè)胖子當(dāng)著我的面吹牛薪棒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播泽篮,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼盗尸,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了帽撑?” 一聲冷哼從身側(cè)響起泼各,我...
    開封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎亏拉,沒想到半個(gè)月后扣蜻,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡及塘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年莽使,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片笙僚。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡芳肌,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出肋层,到底是詐尸還是另有隱情亿笤,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布栋猖,位于F島的核電站净薛,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏蒲拉。R本人自食惡果不足惜肃拜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望雌团。 院中可真熱鬧燃领,春花似錦、人聲如沸锦援。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽雨涛。三九已至枢舶,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間替久,已是汗流浹背凉泄。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蚯根,地道東北人后众。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像颅拦,于是被迫代替她去往敵國和親蒂誉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

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

  • 前言:由于知識(shí)點(diǎn)多距帅,分了多個(gè)記錄。 MMKV( 一) 了解原理[http://www.reibang.com/p...
    zcwfeng閱讀 1,497評(píng)論 2 1
  • POSIX線程 POSIX,全稱為可移植性操作系統(tǒng)接口斟或。它包括了系統(tǒng)應(yīng)用程序接口(簡稱API)艳悔。該標(biāo)準(zhǔn)的目的是定義...
    蔣斌文閱讀 976評(píng)論 0 0
  • MMKV的介紹 MMKV 是基于 mmap 內(nèi)存映射 的 key-value 組件,底層序列化/反序列化使用 p...
    cname_1閱讀 9,718評(píng)論 0 3
  • 前言 好久沒有更新常用的第三方庫了讥电。讓我們來聊聊MMKV這個(gè)常用的第三方庫蹂窖。MMKV這個(gè)庫是做什么的呢?他本質(zhì)上的...
    yjy239閱讀 11,150評(píng)論 7 14
  • SharedPreference 數(shù)據(jù)格式 XML格式保存恩敌,使用Pull解析 初始化 創(chuàng)建SharedPrefer...
    Archer_J閱讀 1,432評(píng)論 2 2