一、持久化數(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)存是隔離的,互相不影響帘瞭。下圖所示:
應(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);
}
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)不同進程間的文件共享回窘。如下圖所示:
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
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);
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ù)速度
五逼友、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)
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ù)共享 |