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)
更高效的文件操作(mmap)
比XML更精簡的數(shù)據(jù)格式(二進(jìn)制逛腿、protobuf)
更優(yōu)的數(shù)據(jù)更新方式(增量更新)
MMKV
MMKV 是基于 mmap 內(nèi)存映射的 key-value 組件,底層序列化/反序列化使用 protobuf 實(shí)現(xiàn)仅颇,性能高单默,穩(wěn)定性強(qiáng)。
I/O
工作原理
- 對(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)核也不受影響
寫文件流程
調(diào)用write向內(nèi)核發(fā)起系統(tǒng)調(diào)用粱年,上下文從用戶態(tài)切換為內(nèi)核態(tài)
CPU將用戶緩沖區(qū)的數(shù)據(jù)拷貝到內(nèi)核空間的緩沖區(qū)(CPU拷貝)
CPU利用 DMA 控制器將數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到磁盤緩沖區(qū)進(jìn)行數(shù)據(jù)傳輸(DMA拷貝)
上下文從內(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)存映射拉庶。
- 對(duì)文件進(jìn)行mmap嗜憔,會(huì)在進(jìn)程的虛擬內(nèi)存分配地址空間,創(chuàng)建映射關(guān)系
- 實(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)系
注:
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
通過mmap方法映射了一個(gè)虛擬文件:/dev/binder
mDriverFD位文件句柄
應(yīng)用
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)制查看工具來看一下:
寫入方式
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ù)同步