使用方法
SharedPreferences
// 1:得到SharedPreferences,這是直接包含在Context中的方式,直接調用便可
// 四種寫入模式:MODE_PRIVATE、MODE_APPEND、MODE_WORLD_READABLE、MODE_WORLD_WRITEABLE
val sp = baseContext.getSharedPreferences("clericyi", Context.MODE_PRIVATE)
// 2:獲取筆,由於第一步得到到至關於一張白紙,須要對應的筆才能對其操做
val editor = sp.edit()
// 3:數據操做,不過咱們當前操做的數據只是一個副本
// putString()、putInt()。。。還有不少方法
editor.putBoolean("is_wirte", true)
// 4:兩種提交方式,將副本內的數據正式寫入實體文件中
editor.commit() // 同步寫入
editor.apply() // 異步寫入
MMKV
第一步:開源庫導入android
implementation 'com.tencent:mmkv-static:1.1.2'
第二步:使用git
// 1. 自定義Aapplication
public void onCreate() {
super.onCreate();
MMKV.initialize(this);
}
// 2. 調度使用
// 和SharedPreferenced同樣,支持的數據類型直接往裏面塞便可
// 不同的地方,MMKV不須要本身去作一些apply()或者是commit()的操做,更加方便
MMKV kv = MMKV.defaultMMKV();
kv.encode("bool", true);
boolean bValue = kv.decodeBool("bool");
爲何這篇文章要拿兩個框架講?
單進程性能github
多進程性能web
不管是單線程仍是多線程,MMKV
的讀寫能力都遠遠的甩開了SharedPreferences
&SQLite
&SQLite+Transacion
,可是MMKV
究竟是如何作到如此快的進行讀寫操做的?這就是下面會經過源碼分析完成的事情了。安全
另外接下來的一句話僅表明了個人我的意見,也是爲何我只寫SharedPreferences
和MMKV
二者比較的緣由,由於我我的認爲SQLite
和他們不太屬於同一類產品,因此比較的意義上來講就趨於普通。微信
SharedPreferences源碼分析
根據上述中所說起過的使用代碼,可以比較清楚的知道第一步的分析對象就是getSharedPreferences()
的獲取操做了,可是若是你直接點進去搜這個方法,是否是會出現這樣的結果呢?多線程
public abstract SharedPreferences getSharedPreferences(String name, @PreferencesMode int mode);
沒錯了,只是一個抽象方法,那顯然如今最重要的事情就是找到他的具體實現類是什麼了,固然你能夠直接查閱資料獲取,最後的正確答案就是ContextImpl
,不知道你有沒有找對呢?併發
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
// .....
// 經過具體實現類,對SharedPreferences進行建立
sp = new SharedPreferencesImpl(file, mode);
// 經過一個cache來防止同一個文件的SharedPreferences的重複建立
cache.put(file, sp);
return sp;
}
}
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
// 若是開啓了多進程模式,一旦數據發生更新,那麼其餘進程的數據會經過重載的方式更新
// 這裏是否存在疑問,爲何網上會說這個方法是一個進程不安全的方案呢?
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
在上面的使用過程當中提到了,其餘他是一個副本的概念,這個從何提及呢?顯然這就要看一下SharedPreferences
的實現類具體是如何進行操做的了,從他的構造函數看起,慢慢進入深度調用。app
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
// 開始從磁盤中調數據
startLoadFromDisk(); // 1 -->
}
// 1 -->
private void startLoadFromDisk() {
// 開啓一條新的線程來加載數據
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk(); // 2-->
}
}.start();
}
// 2 -->
private void loadFromDisk() {
Map<String, Object> map = null;
StructStat stat = null;
Throwable thrown = null;
// 從XML中把數據讀出來,並把數據轉化成Map類型
// 這是一個很是消耗時間的操做
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
str = new BufferedInputStream(
new FileInputStream(mFile), 16 * 1024);
map = (Map<String, Object>) XmlUtils.readMapXml(str);
}
synchronized (mLock) {
mLoaded = true;
mThrowable = thrown;
if (thrown == null) {
// 文件裏拿到的數據爲空就重建,存在就賦值
if (map != null) {
// 將數據存儲放置到具體類的一個全局變量中
// 稍微記一下這個關鍵點
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
}
}
}
到此爲止就基本完成了SharedPreferences
的構建流程,而爲了可以對數據進行操做,那就須要去獲取一隻筆,來進行操做,一樣的這段代碼最後會在SharedPreferencesImpl
中進行具體實現。框架
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
// 很簡單的一個操做,就是建立了一支筆
// 這裏是一個很重要的點,由於每次都是新建立一支筆,因此要作到數據更換的操做要一次性完成。
return new EditorImpl();
}
由於後面的操做都是與這隻筆相關,並且具體操做上重複度比較高,因此只選取一個putString()
來進行分析。
public final class EditorImpl implements Editor {
private final Map<String, Object> mModified = new HashMap<>();
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
}
很簡單的解決思路,就是新建立一個了HashMap
裏面所有保存的都是一些咱們已經作過修改的數據,以後的更新是須要用到這些數據的。
相較於以前的那些源碼,這裏的就顯得很是輕鬆了,結合上述的源碼分析,能夠假設SharedPreferences
氛圍三個要點。
-
mMap: 存儲從文件中拉取的數據。 -
mModified: 存儲但願修改值的數據。 -
apply()/commit(): 猜想最後就是上述二者數據的合併,再進行數據提交。
數據提交
異步提交 / apply()
public void apply() {
// .....
// 這一步其實就是咱們所猜想的第三步中的數據合併
// 作一個簡單的介紹,數據的替換一共分爲三步:
// 1. 將數據存儲到mapToWriteToDisk中
// 2. 與mModified中數據進行比較,不存在或者不一致就替換
// 3. 將更新後得數據返回
final MemoryCommitResult mcr = commitToMemory();
// 經過CountDownLatch來完成數據的同步更新
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
mcr.writtenToDiskLatch.await(); // 1-->
}
};
// 對事件完成的監聽
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
// 經過線程進行異步處理
awaitCommit.run();
// 若是任務完成,就從隊列中清除
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); // 2 -->
// 通知觀察者數據更新
notifyListeners(mcr);
}
從上述的代碼中能夠了解到apply()
的經過建立一個線程來進行處理,以後會講到commit()
和他的處理方式不一樣的地方。如今具體的目光仍是要聚焦在如何完成數據到磁盤的提交的,也就是註釋1處的具體實現究竟是如何?這就是對這個類的一個理解問題了。其實他有點相似於程序計數器,在阻塞數量大於線程數時,會阻塞運行,而超出數量就會出現併發情況。
第二個地方就是註釋2,他線程作了一個入隊列的操做。
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
}
};
// commit()一樣會進入到這個大方法中
// commit()方法執行到這裏運行完就結束,乾的事情就是將數據寫入文件
if (isFromSyncCommit) {
writeToDiskRunnable.run();
return;
}
// apply()多作了層如隊列的操做,意圖在於異步進行
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit); // 3-->
}
// 3 -->
// 由於最後使用的都是其實都是MSG_RUN的參數,因此直接調用查看便可
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();
synchronized (sLock) {
sWork.add(work);
if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY); // 4-->
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN); // 4-->
}
}
}
// 4 -->
public void handleMessage(Message msg) {
if (msg.what == MSG_RUN) {
processPendingWork(); // 5 -->
}
}
// 5 -->
// 就是最後將一個個任務進行完成運行
private static void processPendingWork() {
synchronized (sProcessingWork) {
LinkedList<Runnable> work;
// 。。。。。
if (work.size() > 0) {
for (Runnable w : work) {
w.run(); // 最後將數據一個個進行運行完成操做
}
}
}
}
同步提交 / commit()
public boolean commit() {
MemoryCommitResult mcr = commitToMemory();
// 不須要使用線程來進行異步處理,因此第二參數爲空
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
mcr.writtenToDiskLatch.await();
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
因此說基本邏輯上其實仍是和apply()
方法是一致的,只是去除了異步處理的步驟,因此就是常說的同步處理方式。
總結
-
是什麼限制了 SharedPreferences
的處理速度?
這個問題在上面的源碼分析中其實已經有所說起了,那就是文件讀寫,因此如何加快文件的讀寫速度是一個相當重要的突破點。固然向速度妥協的一個方案,想來你也已經看到了,那就是異步提交,經過子線程的在用戶無感知的狀況下把數據寫到文件中。
-
爲何多線程安全,而多進程不安全的操做?
多線程安全安全想來是一個很是容易解釋的事情了,幹一個很簡單的事情就是synchronized
的加鎖操做,對數據的操做進行加鎖那勢必拿到的最後數據就會是一個安全的數據了。
可是對於多進程呢? 你可能會說在sp.startReloadIfChangedUnexpectedly();
這段代碼出現的難道不是已經涉及了多進程的安全操做嗎?yep!! 若是你想到了這點,說明你有好好看了下代碼,可是沒有看他的實現,若是你去看他的實現方案,就會發現MODE_MULTI_PROCESS
和所可使用的操做的運算結果均爲0
,因此在如今的Android
版本中這是一個被拋棄的方案。固然這是其一,天然還有另一個判斷就是關於版本方面,若是小於HONEYCOMB
一樣能夠進入這個方案,可是須要注意getSharedPreferences()
是隻有獲取時纔會出現的,而SharedPreferences
是對於單進程而言的單獨實例,數據的備份所有在單個進程完成,因此在進行多進程讀寫時,發生錯誤是大機率的。
MMKV源碼分析
初始化 / MMKV.initialize(this);
在MMKV
的整套流程中,MMKV
的初始化起着承上啓下的做用。
public static String initialize(Context context) {
// 獲取根路徑
String root = context.getFilesDir().getAbsolutePath() + "/mmkv";
MMKVLogLevel logLevel = MMKVLogLevel.LevelInfo;
return initialize(root, (MMKV.LibLoader)null, logLevel); // 進行加載 1 -->
}
// 1 -->
public static String initialize(String rootDir, MMKV.LibLoader loader, MMKVLogLevel logLevel) {
// 加載必要的so文件
// 。。。。。
// 經過JNI來對底層c的實現進行初始化調度
jniInitialize(MMKV.rootDir, logLevel2Int(logLevel));
return rootDir;
}
由於到這裏的話直接經過三方庫的導入已經不能知足查看了,因此直接去下載MMKV
的開源庫源碼查看比較合適。
若是你並不太熟悉JNI
的方法調度,也不要緊,我會慢慢的經過方式來教你入門。
你可以發現是爆紅的JNI
方法,那如何定位呢? 摁兩下Shift
的全局搜索,而後直接輸入initializeMMKV
,就會獲得搜索結果了。
可以發現這裏存在兩個方法,進去看看就知道像C
寫的,那目標羣體就已經被你鎖定了。
void MMKV::initializeMMKV(const MMKVPath_t &rootDir, MMKVLogLevel logLevel) {
// ThreadOnce說明初始化過程只會進行一次
ThreadLock::ThreadOnce(&once_control, initialize);
g_rootDir = rootDir;
// 對目標路徑進行設置,層級遞推若是不存在就建立。
mkPath(g_rootDir);
}
對象實例獲取 / MMKV.defaultMMKV()
public static MMKV defaultMMKV() {
// 能夠設置爲多進程模式
// 重點所在
long handle = getDefaultMMKV(SINGLE_PROCESS_MODE, null); // *** -->
return new MMKV(handle); // 1 -->
}
// 1 -->
// 就是一個爲long類型的handle變量設置
private MMKV(long handle) {
nativeHandle = handle;
}
你能看到***
註釋位置是代碼中一個迷惑性行爲,經過數據類型定義可以知道最後獲得的數據是一個數據類型爲long
的數據,咱們能夠猜想這個數據的用處對應着最後可以用於尋找到對應的MMKV
,經過深層次調用後能夠發現他調用了一個mmkvWithID()
的方法,其中DEFAULT_MMAP_ID
爲mmkv.default
。
MMKV *MMKV::mmkvWithID(const string &mmapID, int size, MMKVMode mode, string *cryptKey, string *relativePath) {
// 。。。。。。
auto mmapKey = mmapedKVKey(mmapID, relativePath); // 1 -->
auto itr = g_instanceDic->find(mmapKey);
if (itr != g_instanceDic->end()) {
MMKV *kv = itr->second; // 2 -->
return kv;
}
if (relativePath) {
if (!isFileExist(*relativePath)) {
if (!mkPath(*relativePath)) {
return nullptr;
}
}
}
auto kv = new MMKV(mmapID, size, mode, cryptKey, relativePath); // 3 -->
(*g_instanceDic)[mmapKey] = kv;
return kv;
}
在這個代碼中總共有兩個核心部分:
-
mmapKey的值的計算: 經過 mmapID
和relativePath
兩個值進行必定的運算操做,具體關係就是mmapID
和relativePath
的重合關係,具體仍是要見於代碼實現。 -
MMKV的生成: 這裏的解釋對應 註釋2
和註釋3
,就是經過一個Map
的形式來對數據進行存儲,若是在g_instanceDic
這個變量中進行數據查詢。
MMKV的內部結構
MMKV::MMKV(const string &mmapID, int size, MMKVMode mode, string *cryptKey, string *relativePath)
: m_mmapID(mmapedKVKey(mmapID, relativePath)) // historically Android mistakenly use mmapKey as mmapID
, m_path(mappedKVPathWithID(m_mmapID, mode, relativePath)) // 1 -->
, m_crcPath(crcPathWithID(m_mmapID, mode, relativePath))
, m_dic(nullptr)
, m_dicCrypt(nullptr)
, m_file(new MemoryFile(m_path, size, (mode & MMKV_ASHMEM) ? MMFILE_TYPE_ASHMEM : MMFILE_TYPE_FILE))
, m_metaFile(new MemoryFile(m_crcPath, DEFAULT_MMAP_SIZE, m_file->m_fileType))
, m_metaInfo(new MMKVMetaInfo())
, m_crypter(nullptr)
, m_lock(new ThreadLock())
, m_fileLock(new FileLock(m_metaFile->getFd(), (mode & MMKV_ASHMEM)))
, m_sharedProcessLock(new InterProcessLock(m_fileLock, SharedLockType))
, m_exclusiveProcessLock(new InterProcessLock(m_fileLock, ExclusiveLockType))
, m_isInterProcess((mode & MMKV_MULTI_PROCESS) != 0 || (mode & CONTEXT_MODE_MULTI_PROCESS) != 0) {
m_actualSize = 0;
m_output = nullptr;
if (cryptKey && cryptKey->length() > 0) {
m_dicCrypt = new MMKVMapCrypt();
m_crypter = new AESCrypt(cryptKey->data(), cryptKey->length());
} else {
m_dic = new MMKVMap();
}
// 。。。。。。一些賦值操做
// sensitive zone
{
SCOPED_LOCK(m_sharedProcessLock);
loadFromFile();
}
}
和SharedPreferences
相同最後仍是須要經歷一場和文件讀寫的殊死搏鬥,那問題就來了,一樣是文件讀寫,爲何MMKV
可以以百倍的速度碾壓各種已成熟的產品呢?從個人思路出發能夠分爲這樣的幾種狀況:
-
不夠健壯的錯誤數據處理。 這若是你作一個簡易版的 FastJson
就可以發現,數據的處理速度基本上可以有很是高的提高。可是這對於相對成熟的產品而言通常不會有這種方案。 -
底層進行數據處理。 這個方案的推行在必定程度上也是對應如今的二者對比有必定的道理,由於可以發現 MMKV
的實現方案基本都是依靠JNI
來調度完成,而C
的處理速度和Java
相比想來咱們也是有目共睹的。 -
更優化的文件讀取方案。 這就是對當前方案的分析了,由於尚未看到後面的代碼,因此這裏是一種方案的猜想。由於 SharedPreferences
和MMKV
二者都是咱們有目共睹須要對數據進行讀寫操做的,而數據的最後來源就是本地的文件,一個更易於讀寫的文件方案勢必是一個最關鍵的突破點。 -
。。。。。接下來由你開始進行更多的思考。
迴歸正題:loadFromFile();
在剛剛的猜測中,我說起了關於文件讀寫的問題,由於對MMKV
而言,文件讀寫這一關確定是躲不過去的,可是如何更高效就是咱們應該去思考的點了。
void MMKV::loadFromFile() {
// 文件不合法就從新加載
if (!m_file->isFileValid()) {
m_file->reloadFromFile();
}
// 文件依舊不合法就報錯
if (!m_file->isFileValid()) {
MMKVError("file [%s] not valid", m_path.c_str());
} else {
// 進入這一步至少說明文件是合法的,可是須要進行數據的校驗
// error checking
bool loadFromFile = false, needFullWriteback = false;
checkDataValid(loadFromFile, needFullWriteback);
auto ptr = (uint8_t *) m_file->getMemory();
// loading
if (loadFromFile && m_actualSize > 0) {
MMBuffer inputBuffer(ptr + Fixed32Size, m_actualSize, MMBufferNoCopy);
if (m_crypter) {
clearDictionary(m_dicCrypt);
} else {
clearDictionary(m_dic);
}
// 1 -->
if (needFullWriteback) {
if (m_crypter) {
MiniPBCoder::greedyDecodeMap(*m_dicCrypt, inputBuffer, m_crypter); // 2 -->
} else {
MiniPBCoder::greedyDecodeMap(*m_dic, inputBuffer); // 2 -->
}
} else {
// 1 -->
if (m_crypter) {
MiniPBCoder::decodeMap(*m_dicCrypt, inputBuffer, m_crypter); // 2 -->
} else {
MiniPBCoder::decodeMap(*m_dic, inputBuffer); // 2 -->
}
}
m_output = new CodedOutputData(ptr + Fixed32Size, m_file->getFileSize() - Fixed32Size);
m_output->seek(m_actualSize); // 計算出數據量的實際大小
if (needFullWriteback) {
fullWriteback();
}
} else {
// 若是數據是不合法或者空的就直接丟棄。
// 。。。。。。
}
}
m_needLoadFromFile = false;
}
在代碼段中我標註出了註釋1
和註釋2
,也是我認爲相當重要的代碼了,分別作了兩大操做:
-
數據的寫回方案製做: 這是要一個很是有特點的地方,爲何這麼說呢?其實你可以從一個判斷的變量名可以看出會對數據的寫回方式有一個選擇,也就是部分寫回和所有寫回的策略之選,那這就是第一個緣由爲何 MMKV
的綜合性能可以強過SharedPreferences
。 -
文件格式的選擇: 其實這是解析時候的事情了,這一段的論證來源於 MMKV 原理 [1], protobuf
做爲MMKV
最後的選擇方案在性能和空間佔用上都有不錯的表現。
數據更新 / kv.encodeXXX("string", XXX);
這裏的代碼分析只拿一個做爲樣例便可
MMKV_JNI jboolean encodeBool(JNIEnv *env, jobject, jlong handle, jstring oKey, jboolean value) {
MMKV *kv = reinterpret_cast<MMKV *>(handle); // 1-->
if (kv && oKey) {
string key = jstring2string(env, oKey); // 將key再進行特殊的加工處理
return (jboolean) kv->set((bool) value, key); // 2 -->
}
return (jboolean) false;
}
關注幾個註釋點:
-
註釋1: 這就是以前在上面的時候已經提到過的在 Java
這一層中進行的操做只是一個數據類型爲long
的handle
變量進行賦值操做,而這個handle
中在後期能夠被解析轉化爲已經初始化完成的MMKV
對象。 -
註釋2: 完成相對應的數據放置操做,那這裏就要觀察代碼的深層調度是一個怎麼樣的過程了。
bool MMKV::set(bool value, MMKVKey_t key) {
// 1. 進行數據的測量,並建立相同大小的區間
size_t size = pbBoolSize();
MMBuffer data(size);
// 2. 轉化爲CodedOutputData對象用於寫入
CodedOutputData output(data.getPtr(), size);
output.writeBool(value); // 3 -->
// 從名字就能知道這實際上是一個正式的數據替換操做
// 追溯後能夠發現會出現一個文件的寫入。
return setDataForKey(move(data), key);
}
// 3-->
void CodedOutputData::writeBool(bool value) {
// 用0和1來表示最後的數值
this->writeRawByte(static_cast<uint8_t>(value ? 1 : 0));
}
可是經過官方的文檔中可以知道,關於這個文件格式下的數據是存在問題的,那就是他並不支持增量更新 ,這也就意味着複雜的操做會更加多了,那騰訊的解決方案是什麼呢?
標準
protobuf
不提供增量更新的能力,每次寫入都必須全量寫入。考慮到主要使用場景是頻繁地進行寫入更新,咱們須要有增量更新的能力:將增量kv
對象序列化後,直接append
到內存末尾;這樣同一個key
會有新舊若干份數據,最新的數據在最後;那麼只需在程序啓動第一次打開mmkv
時,不斷用後讀入的value
替換以前的值,就能夠保證數據是最新有效的。
一句話講來就是,新的或更改過的就最後新增後面插入。
而新舊數據累加勢必會形成文件的龐大,那這方面MMKV
給出的解決方案又是怎麼樣的呢?
之內存
pagesize
爲單位申請空間,在空間用盡以前都是append
模式;當append
到文件末尾時,進行文件重整、key
排重,嘗試序列化保存排重結果;排重後空間仍是不夠用的話,將文件擴大一倍,直到空間足夠。
一樣的換成一句話來進行描述,有上限目標的文件重寫。
這一段的代碼實現就不貼出了,具體位置就在MMKV_IO
中的ensureMemorySize()
方法,經過已存在數據大小的總量來進行整理,由於不少時候數據量很大是由於大容量的數據的重複添加形成的。
數據獲取 / kv.decodeXXX("string");
MMKV_JNI jboolean decodeBool(JNIEnv *env, jobject, jlong handle, jstring oKey, jboolean defaultValue) {
MMKV *kv = reinterpret_cast<MMKV *>(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
return (jboolean) kv->getBool(key, defaultValue);
}
return defaultValue;
}
其實基本邏輯和寫文件的差很少了,這個時候仍是首先要獲取一個對應的MMKV
對象,而後完成數據的獲取。
bool MMKV::getBool(MMKVKey_t key, bool defaultValue) {
auto data = getDataForKey(key);
if (data.length() > 0) {
CodedInputData input(data.getPtr(), data.length());
return input.readBool();
}
return defaultValue;
}
轉化爲CodedInputData
的對象來完成數據的讀取,若是數據不存在,那就直接默認值返回。
刪除對應的數據 / kv.removeValueForKey("string")
在看代碼以前作一個思考,在已知的數據基礎上,換成你會怎麼作這樣的操做呢?
咱們要關注的點有如下幾個:
-
protobuf
是一個不支持增量更新的文件格式,相對應MMKV
給出的解決方案就是經過尾部增長,出現新舊數據疊加 -
從 問題1
的引伸,新舊數據疊加的一個查詢和刪除問題,由於新舊數據,那麼作查詢的時候勢必要屢次的查,若是每次的數據都有1G
,那你的查詢每次都要疊加到1G
的程度,而不是查到便可開始刪除。
對於以上問題思考清楚了的話,咱們就能夠給出MMKV
的解決方案了。
auto itr = m_dic->find(key);
if (itr != m_dic->end()) {
m_hasFullWriteback = false;
static MMBuffer nan; // ******
auto ret = appendDataWithKey(nan, itr->second); // ******
if (ret.first) {
#ifdef MMKV_APPLE
[itr->first release];
#endif
m_dic->erase(itr);
}
return ret.first;
}
將關注點所有放置於註釋帶*
的代碼段上,一個沒有賦值的MMBuffer
說明數據爲空,而後直接調用appendDataWithKey()
文件寫入的方案,說明最後出如今protobuf
的數據樣式會是這樣的。
message empty{
}
其實就是往裏面加一個新的空數據做爲新的數據。
總結
從源碼分析完以後,和SharedPreferences
相比,從新整理後能夠總結爲如下幾點的突破:
-
mmap
的使用: 內存映射的技術的使用,減小了SharedPreferences
的拷貝和提交的時間消耗。 -
數據的更新方式: 局部更新的數據,經過尾部追加來進行完成,而不是像 SharedPreferences
同樣的直接文件重構。一樣要注意這樣的方式會形成冗餘數據的增長。 -
多進程訪問安全的設計: 詳細見於 MMKV for Android 多進程設計與實現 [2],主要仍是以 mmap
做爲突破口,來完成對其餘進程對當前文件的操做的一個狀態感知,主要就是分爲三方面: 寫指針增加、內存重整、內存增加 。
參考資料
-
MMKV官方文檔 [3]
參考資料
MMKV 原理: https://github.com/Tencent/MMKV/wiki/design#mmkv-原理
[2]MMKV for Android 多進程設計與實現: https://github.com/Tencent/MMKV/wiki/android_ipc
[3]MMKV官方文檔: https://github.com/Tencent/MMKV/wiki
本文分享自微信公衆號 - 告物(ClericYi_Android)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。