SharedPreference與MMKV

SharedPreference

  • 數(shù)據(jù)格式

XML格式保存,使用Pull解析

  • 初始化

創(chuàng)建SharedPreferencesImpl時(shí)解析數(shù)據(jù)械馆,子線程使用Java IO讀取整個(gè)文件朵耕,進(jìn)行XML解析,并將所有數(shù)據(jù)存入內(nèi)存Map集合,其他操作都需要等待初始化完成

  • 保存

commit同步提交,阻塞調(diào)用線程

apply異步提交撑蚌,通過HandlerThread創(chuàng)建子線程

  • 更新

把Map中的數(shù)據(jù),全部序列化為XML搏屑,覆蓋文件保存

  • 不支持多進(jìn)程

可能出現(xiàn)ANR

調(diào)用apply方法異步提交數(shù)據(jù)

// SharedPreferencesImpl#EditorImpl#apply
public void apply() {
    ...
  final Runnable awaitCommit = new Runnable(){
    ...
  }
  // 將runnable添加進(jìn)隊(duì)列中
    QueuedWork.addFinisher(awaitCommit);
  ...
  // 通過HandlerThread執(zhí)行IO操作
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
    ...
}
  • Android是基于消息驅(qū)動(dòng)的,所有代碼都是由Handler驅(qū)動(dòng)執(zhí)行的粉楚,Activity生命周期也不例外辣恋。

  • Activity啟動(dòng)流程中,我們知道Activity生命周期最終會(huì)由ActivityThread中的一個(gè)Handler發(fā)送到主線程執(zhí)行模软。其中onStop時(shí)執(zhí)行handleStopActivity伟骨。

  • 回調(diào)onStop之后,如果QueuedWork中有未完成的任務(wù)燃异,則會(huì)同步執(zhí)行其中的任務(wù)携狭。

  • 所以,如果任務(wù)耗時(shí)過長回俐,則可能出現(xiàn)ANR

@Override
public void handleStopActivity(IBinder token, int configChanges,
      PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {
   ...
   // 回調(diào)onStop
   performStopActivityInner(...);         
   ...
   // 阻塞等待隊(duì)列執(zhí)行完畢
   QueuedWork.waitToFinish();
}

優(yōu)化方向(MMKV)

  1. 更高效的文件操作(mmap)

  2. 比XML更精簡的數(shù)據(jù)格式(二進(jìn)制逛腿、protobuf)

  3. 更優(yōu)的數(shù)據(jù)更新方式(增量更新)

MMKV

MMKV 是基于 mmap 內(nèi)存映射的 key-value 組件,底層序列化/反序列化使用 protobuf 實(shí)現(xiàn)仅颇,性能高单默,穩(wěn)定性強(qiáng)。

Github

I/O

工作原理

image1.png
  • 對(duì)文件的操作只有內(nèi)核才能執(zhí)行
  • 虛擬內(nèi)存被操作系統(tǒng)劃分為兩塊:用戶空間內(nèi)核空間忘瓦,用戶空間時(shí)用戶程序代碼運(yùn)行的地方搁廓,內(nèi)核空間是內(nèi)核代碼運(yùn)行的地方,內(nèi)核空間由所有進(jìn)程共享。為了安全境蜕,它們是隔離(內(nèi)存隔離)的蝙场,即使用戶程序崩潰了,內(nèi)核也不受影響

寫文件流程

  1. 調(diào)用write向內(nèi)核發(fā)起系統(tǒng)調(diào)用粱年,上下文從用戶態(tài)切換為內(nèi)核態(tài)

  2. CPU將用戶緩沖區(qū)的數(shù)據(jù)拷貝到內(nèi)核空間的緩沖區(qū)(CPU拷貝)

  3. CPU利用 DMA 控制器將數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到磁盤緩沖區(qū)進(jìn)行數(shù)據(jù)傳輸(DMA拷貝)

  4. 上下文從內(nèi)核態(tài)切換回用戶態(tài)售滤,write系統(tǒng)調(diào)用執(zhí)行返回

寫文件經(jīng)歷了兩次拷貝:

  • CPU拷貝,數(shù)據(jù) -> 內(nèi)核
  • DMA拷貝逼泣,內(nèi)核 -> 文件

注:SP是基于I/O的存儲(chǔ)方式

mmap(memory mapping 內(nèi)存映射)

原理

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

image
  1. 對(duì)文件進(jìn)行mmap嗜憔,會(huì)在進(jìn)程的虛擬內(nèi)存分配地址空間,創(chuàng)建映射關(guān)系
  2. 實(shí)現(xiàn)這樣的映射關(guān)系后氏仗,就可以采用指針的方式讀寫操作這一段內(nèi)存吉捶,而系統(tǒng)會(huì)自動(dòng)回寫到對(duì)應(yīng)的文件磁盤上

注:mmap的關(guān)鍵點(diǎn)是實(shí)現(xiàn)了用戶空間和內(nèi)核空間的數(shù)據(jù)直接交互而省去了空間不同數(shù)據(jù)不同的繁瑣過程

優(yōu)勢(shì)

  • MMAP對(duì)文件的讀寫操作只需要從磁盤到用戶主存的一次數(shù)據(jù)拷貝過程,減少了數(shù)據(jù)的拷貝次數(shù)皆尔,提高了文件操作效率

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

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

注:MMAP是零拷貝的(不需要CPU參與的拷貝),也可理解為一次拷貝(/DMA拷貝)

Binder

Binder是基于mmap實(shí)現(xiàn)的跨進(jìn)程通訊機(jī)制

Activity啟動(dòng)過程中每辟,ZygoteInit.nativeZygoteInit() 調(diào)用c++代碼創(chuàng)建Binder對(duì)象

具體過程為:

-> zygote fork 一個(gè)應(yīng)用進(jìn)程

-> RuntimeInit

-> ZyzoteInit

-> ProcessState(進(jìn)程狀態(tài)對(duì)象)剑辫,一個(gè)進(jìn)程會(huì)有一個(gè)ProcessState對(duì)象

// ZygoteInit#zygoteInit
public static final Runnable zygoteInit(int targetSdkVersion, String[] argv,
            ClassLoader classLoader) {
        // 為當(dāng)前的VM設(shè)置未捕獲異常器
    RuntimeInit.commonInit();
        // Binder驅(qū)動(dòng)初始化,該方法完成后渠欺,可通過Binder進(jìn)行進(jìn)程通信
    ZygoteInit.nativeZygoteInit();
        // 主要調(diào)用SystemServer的main方法
    return RuntimeInit.applicationInit(targetSdkVersion, argv, classLoader);
}

ZygoteInit.nativeZygoteInit() 方法調(diào)用native創(chuàng)建ProcessState并創(chuàng)建了內(nèi)存映射關(guān)系

image3.png

注:

DEFAULT_BINDER_VM_SIZE 為Binder傳輸數(shù)據(jù)的大小限制

_SC_PAGE_SIZE為一頁妹蔽,一般為4096個(gè)字節(jié)(4K)

所以:Binder默認(rèn)能傳輸?shù)拇笮椋?strong>1M - 8k,這里指的是同步方式

異步(aidl 指定 oneway):(1M-8K) / 2 = 500+K

同步:1M-8K

image2.png

通過mmap方法映射了一個(gè)虛擬文件:/dev/binder

mDriverFD位文件句柄

應(yīng)用

微信Mars

美團(tuán)Logan

網(wǎng)易七魚

愛奇藝xCrash

Protobuf(變長編碼)

protobuf 是google開源的一個(gè)序列化框架挠将,類似xml胳岂,json,最大的特點(diǎn)是基于二進(jìn)制舔稀,比傳統(tǒng)的XML表示同樣一段內(nèi)容要短小得多旦万。

MMKV正式基于protobuf協(xié)議進(jìn)行數(shù)據(jù)存儲(chǔ),存儲(chǔ)方式為增量更新镶蹋,也就是不需要每次修改數(shù)據(jù)都要重新將所有數(shù)據(jù)寫入文件了成艘。

為什么使用二進(jìn)制

一個(gè)字節(jié) = 8位

整數(shù)1 = 4個(gè)字節(jié)32位赏半,轉(zhuǎn)化為二進(jìn)制:0000 0000 0000 0000 0000 0000 0000 0001

如果使用二進(jìn)制格式,即可用一個(gè)字節(jié)表示:0000 0001

數(shù)據(jù)更緊湊淆两、精簡了

場景:

http1:使用字符串文本格式傳輸

http2:使用二進(jìn)制格式傳輸

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

protobuf是二進(jìn)制存儲(chǔ)格式断箫,第一位代表的是key和value的總長度,后面是key長度->key秋冰, value長度->value仲义。。剑勾。埃撵。。 依次排列虽另,可以用二進(jìn)制查看工具來看一下:

image

寫入方式

1個(gè)字節(jié)8位暂刘,低7位是數(shù)據(jù)位,第1位為標(biāo)志位(0表示讀取截止捂刺,1表示需要繼續(xù)讀取)

編解碼過程看這里

擴(kuò)容

Linux采用了分頁來管理內(nèi)存谣拣,存入數(shù)據(jù)先要?jiǎng)?chuàng)建一個(gè)文件,并要給這個(gè)文件分配一個(gè)固定的大小族展。如果存入了一個(gè)很小的數(shù)據(jù)森缠,那么這個(gè)文件其余的內(nèi)存就會(huì)被浪費(fèi)。相反如果存入的數(shù)據(jù)比文件大仪缸,就需要?jiǎng)討B(tài)擴(kuò)容贵涵。

增量更新

  • 寫入優(yōu)化

將增量key-value對(duì)象序列化后,append 到內(nèi)存文件

由于數(shù)據(jù)讀取出來后恰画,會(huì)放入一個(gè)map集合宾茂,這樣后面的數(shù)據(jù)就會(huì)覆蓋前面的數(shù)據(jù),所以總能拿到最新的值

  • 當(dāng)數(shù)據(jù)大于文件時(shí)锣尉,需要進(jìn)行擴(kuò)容,然后重新進(jìn)行mmap映射

多進(jìn)程

具體過程參考這里

flock文件鎖

處理進(jìn)程間的同步時(shí)使用了flock文件鎖

文件鎖的使用:

//通過open方法打開一個(gè)文件
string m_path;
int m_fd = open(m_path.c_str(), O_RDWR | O_CREAT, S_IRWXU);
//通過文件句柄對(duì)文件上鎖
int flock(m_fd, operation);

其中的參數(shù)operation是上鎖的類型

LOCK_SH, 共享鎖,多個(gè)進(jìn)程可以同時(shí)使用,可以作為讀鎖

LOCK_EX, 排他鎖,同時(shí)只允許一個(gè)進(jìn)程使用,可以作為寫鎖

LOCK_UN, 解鎖

LOCK_BN, 非阻塞請(qǐng)求, 與讀寫鎖配合使用

使用flock對(duì)一個(gè)文件上讀鎖,或者寫鎖,都是會(huì)阻塞的,比如A進(jìn)程持有一個(gè)文件的寫鎖,B進(jìn)程想要對(duì)這個(gè)文件上寫鎖,就會(huì)阻塞住,如果不想被阻塞,可以配合LOCK_BN屬性使用,即LOCK_BN | LOCK_EX.

flock有幾個(gè)特點(diǎn):

  • flock支持對(duì)一個(gè)文件多次上鎖,并且因?yàn)槭菭顟B(tài)鎖,沒有計(jì)數(shù)器,不管加了多少次鎖,都只需要解鎖一次.所以,mmkv中對(duì)flock封裝時(shí),加了計(jì)數(shù)器,就是保證上了幾次鎖,就要執(zhí)行幾次解鎖.
  • 鎖升級(jí),降級(jí),當(dāng)一個(gè)進(jìn)程對(duì)一個(gè)文件加了讀鎖后,如果再次執(zhí)行flock操作,傳入的operation是LOCK_EX,那么這個(gè)進(jìn)程對(duì)文件的讀鎖就升級(jí)為了寫鎖,這就是鎖升級(jí),反之,就是鎖降級(jí),但是文件鎖降級(jí)是無法進(jìn)行的,因?yàn)樗恢С诌f歸,導(dǎo)致一降級(jí)就沒鎖了.

為了解決上面的問題,mmkv對(duì)文件鎖進(jìn)行了封裝,增加了讀寫鎖計(jì)數(shù)器,支持遞歸

文件校驗(yàn)

利用文件鎖可以實(shí)現(xiàn)同一時(shí)間只有一個(gè)進(jìn)程對(duì)file進(jìn)行操作了决采,但是A進(jìn)程修改了文件后,B進(jìn)程怎么知道這個(gè)修改呢?

mmkv并沒有去對(duì)保存key-value數(shù)據(jù)的那個(gè)文件枷鎖自沧,而是鎖了個(gè).crc校驗(yàn)文件.這個(gè)校驗(yàn)文件就是來解決上面的問題的.

struct MMKVMetaInfo {
    uint32_t m_crcDigest = 0;
    uint32_t m_version = 1;
    uint32_t m_sequence = 0; // full write-back count
    unsigned char m_vector[AES_KEY_LEN] = {0};
}
  • 這個(gè)mmkv.default.crc文件中有一個(gè)校驗(yàn)碼,就是mmkv.default文件的MD5值,通過他可以判斷文件是否合法.

  • 還有一個(gè)序列號(hào),當(dāng)序列化文件進(jìn)行了去重,擴(kuò)容操作,這個(gè)序列號(hào)就會(huì)增加,

  • 當(dāng)mmkv初始化時(shí),會(huì)讀取crc文件,記錄其中的校驗(yàn)碼,序列號(hào),,

  • 當(dāng)要讀取數(shù)據(jù)時(shí),會(huì)通過checkLoadData來做校驗(yàn)

如果序列號(hào)不一致,說明發(fā)生了內(nèi)存重整,重新讀取整個(gè)文件

  • 如果文件大小不一致了,可能發(fā)生了擴(kuò)容,這時(shí)重新加載整個(gè)文件,
  • 文件大小一致,說`明只是新增了k-v,也即是增量更新,這時(shí)只要加載新增加的數(shù)據(jù).

通過校驗(yàn)文件,在讀取數(shù)據(jù)時(shí),來做校驗(yàn),就實(shí)現(xiàn)了多個(gè)進(jìn)程的數(shù)據(jù)同步.

總結(jié)

  • mmap提高讀寫效率

  • protobuf(變長編碼)精簡數(shù)據(jù)树瞭,非類型安全

  • 增量更新拇厢,避免每次進(jìn)行大數(shù)據(jù)量的全量寫入

  • 文件鎖+校驗(yàn)碼(md5),保證多進(jìn)程數(shù)據(jù)同步

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末晒喷,一起剝皮案震驚了整個(gè)濱河市孝偎,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌凉敲,老刑警劉巖衣盾,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件寺旺,死亡現(xiàn)場離奇詭異,居然都是意外死亡势决,警方通過查閱死者的電腦和手機(jī)阻塑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來果复,“玉大人陈莽,你說我怎么就攤上這事∷涑” “怎么了走搁?”我有些...
    開封第一講書人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長迈窟。 經(jīng)常有香客問我私植,道長,這世上最難降的妖魔是什么菠隆? 我笑而不...
    開封第一講書人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任兵琳,我火速辦了婚禮,結(jié)果婚禮上骇径,老公的妹妹穿的比我還像新娘躯肌。我一直安慰自己,他們只是感情好破衔,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開白布清女。 她就那樣靜靜地躺著,像睡著了一般晰筛。 火紅的嫁衣襯著肌膚如雪嫡丙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,165評(píng)論 1 299
  • 那天读第,我揣著相機(jī)與錄音曙博,去河邊找鬼。 笑死怜瞒,一個(gè)胖子當(dāng)著我的面吹牛父泳,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播吴汪,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼惠窄,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了漾橙?” 一聲冷哼從身側(cè)響起杆融,我...
    開封第一講書人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎霜运,沒想到半個(gè)月后脾歇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蒋腮,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年介劫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了徽惋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡座韵,死狀恐怖险绘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情誉碴,我是刑警寧澤宦棺,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站黔帕,受9級(jí)特大地震影響代咸,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜成黄,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一呐芥、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧奋岁,春花似錦思瘟、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蓝翰,卻和暖如春光绕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背畜份。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來泰國打工诞帐, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人爆雹。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓停蕉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親顶别。 傳聞我的和親對(duì)象是個(gè)殘疾皇子谷徙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353