??????SharedPreferences 作為輕量級存儲在 Android 應(yīng)用中是必不可少的,但依舊存在較大的優(yōu)化空間,小菜在做性能優(yōu)化時嘗試了新的利器 騰訊 MMKV,小菜今天按如下腦圖順序嘗試學習和簡單分析一下;

SharedPreferences
1. SharedPreferences 基本介紹
??????SharedPreferences 是一種輕量級存儲方式,以 key-value 方式存儲在本地 xml 文件中;其持久化的本質(zhì)就是在在本地磁盤記錄一個 xml 文件;
public interface SharedPreferences {
public interface OnSharedPreferenceChangeListener {
void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);
}
void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
Editor edit();
public interface Editor {
Editor putString(String key, @Nullable String value);
Editor putStringSet(String key, @Nullable Set<String> values);
...
Editor remove(String key);
Editor clear();
boolean commit();
void apply();
}
}
??????簡單分析源碼可得,SharedPreferences 只是一個接口,SharedPreferencesImpl 為具體的實現(xiàn)類,通過 ContextImpl 中 getSharedPreferences() 獲取對象;
2. SharedPreferences 初始化
SharedPreferences sp = getSharedPreferences(Constants.SP_APP_CONFIG, MODE_PRIVATE);
??????SharedPreferences 的通過 getSharedPreferences() 初始化創(chuàng)建一個對象;其中 MODE 為文件操作類型;MODE_PRIVATE 為本應(yīng)用私有的,其他 app 不可訪問的;MODE_APPEND 也為應(yīng)用私有,但是新保存的數(shù)據(jù)放置在文件最后,不會替換之前已有的 key-value;MODE_WORLD_READABLE/WRITEABLE 為其他文件是否可以支持讀寫操作;常用的還是 MODE_PRIVATE 方式;

@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
if (mPackageInfo.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.KITKAT) {
if (name == null) { name = "null"; }
}
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
// TAG 01
mSharedPrefsPaths = new ArrayMap<>();
}
file = mSharedPrefsPaths.get(name);
if (file == null) {
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
// TAG 02
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
checkMode(mode);
if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
if (isCredentialProtectedStorage() && !getSystemService(UserManager.class).isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
throw new IllegalStateException("SharedPreferences in credential encrypted storage are not available until after user is unlocked");
}
}
// TAG 03
sp = new SharedPreferencesImpl(file, mode);
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;
}
??????小菜在源碼處注明了幾個 TAG 需要注意的地方;
TAG 01: 在根據(jù) name 查詢文件時,SharedPreferences 使用了 ArrayMap,相較于 HashMap 更便捷,更節(jié)省空間;

TAG 02: 在創(chuàng)建生成 SharedPreferences 時,通過 cache 來防止同一個 SharedPreferences 被重復(fù)創(chuàng)建;
TAG 03: SharedPreferencesImapl 為具體的實現(xiàn)類,初始化時開啟新的 I/O 線程讀取整個文件 startLoadFromDisk(),進行 xml 解析,存入內(nèi)存 Map 集合中;
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk();
}
private void startLoadFromDisk() {
synchronized (mLock) { mLoaded = false; }
new Thread("SharedPreferencesImpl-load") {
public void run() { loadFromDisk(); }
}.start();
}
3. SharedPreferences 編輯提交
// 編輯數(shù)據(jù)
Editor editor = sp.edit();
editor.putString("name", "阿策小和尚");
// 提交數(shù)據(jù)
editor.apply();
// 獲取數(shù)據(jù)
Editor editor = sp.edit();
editor.getString("name", "");
??????Editor 是用于編輯 SharedPreferences 內(nèi)容的接口,EditorImpl 為具體的實現(xiàn)類;putXXX() 編輯后的數(shù)據(jù)保存在 Editor 中,commit()/apply() 后才會更新到 SharedPreferences;
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
@GuardedBy("mLock")
private void awaitLoadedLocked() {
if (!mLoaded) {
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
??????getXXX() 獲取數(shù)據(jù)時根據(jù) mLoaded 文件是否讀取完成判斷,若未讀取完成 awaitLoadedLocked() 會被阻塞,此時在 UI 主線程中進行使用時就可有可能會造成 ANR;
@Override
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
??????Editor 通過 commit() 和 apply() 提交更新到 SharedPrefenences;兩者的區(qū)別很明顯,apply() 通過線程進行異步處理,如果任務(wù)完成則從隊列中移除 QueuedWork.removeFinisher,無法獲取提交的結(jié)果;commit 是同步更新,使用時會阻塞主線程,因為是同步提交,可以獲取 Boolean 狀態(tài)的提交狀態(tài),進而判斷是否提交成功;
4. SharedPreferences 問題與優(yōu)化
??????SharedPreferences 雖因其便利性而應(yīng)用廣泛,但也存在一些弊端;
Q1: 編輯 get()/put() 時均會涉及到互斥鎖和寫入鎖,并發(fā)操作時影響性能;
A1: 讀寫操作都是針對的 SharedPreferences 對象,可適當拆分文件或降低訪問頻率等;
Q2: 使用時出現(xiàn)卡頓引發(fā) GC 或 ANR;
A2:
- 不要存放大數(shù)據(jù)類型的 key-value 避免導(dǎo)致一直在內(nèi)存中無法釋放;
- 盡量避免頻繁讀寫操作;
- 盡量減少 apply() 次數(shù),每次都會新建一個 EditorImpl 對象,可以批量處理統(tǒng)一提交;
Q3: 不能跨進程通信,不能保證更新本地數(shù)據(jù)后被另一個進程所知;
A3: 可以借助 ContentProvider 來在多進程中更新數(shù)據(jù);
MMKV
1. MMKV 基本介紹
??????正因為 SharedPreferences 還有很大的優(yōu)化空間,因為我們才會嘗試其他存儲框架;其中 騰訊 MMKV 得到很多人的支持;
??????MMKV 分別代表的是 Memory Mapping Key Value,是基于 mmap 內(nèi)存映射的 key-value 組件,底層序列化/反序列化使用 protobuf 實現(xiàn),性能高,穩(wěn)定性強;官網(wǎng) Wiki 介紹的優(yōu)勢很明顯,是目前微信正在使用的輕量級存儲框架;在 Android / macOS / Win32 / POSIX 多個平臺一并開源;
2. MMKV 優(yōu)勢
??????小菜從如下幾個角度簡單分析一下 MMKV 的優(yōu)勢;
a. 數(shù)據(jù)格式及更新范圍優(yōu)化;
??????SharedPreferences 采用 xml 數(shù)據(jù)存儲,每次讀寫操作都會全局更新;MMKV 采用 protobuf 數(shù)據(jù)存儲,更緊密,支持局部更新
b. 文件耗時操作優(yōu)化;
??????MMKV 采用 MMap 內(nèi)存映射的方式取代 I/O 操作,使用 0拷貝技術(shù)提高更新速度;
c. 跨進程狀態(tài)同步;
??????SharedPreferences 為了線程安全不支持跨進程狀態(tài)同步;MMKV 通過 CRC 校驗 和文件鎖 flock 實現(xiàn)跨進程狀態(tài)更新;
d. 應(yīng)用便捷性,較好的兼容性;
??????MMKV 使用方式便捷,與 SharedPreferences 基本一致,遷移成本低;

2.1 Memory Mapping 內(nèi)存映射
??????Memory Mapping 簡稱 MMap 是一種將磁盤上文件的一部分或整個文件映射到應(yīng)用程序地址空間的一系列地址機制,從而應(yīng)用程序可以用訪問內(nèi)存的方式訪問磁盤文件;

??????由此可見,MMap 的優(yōu)勢很明顯了,因為進行了內(nèi)存映射,操作內(nèi)存相當于操作文件,無需開啟新的線程,相較于 I/O 對文件的讀寫操作只需要從磁盤到用戶主存的一次數(shù)據(jù)拷貝過程,減少了數(shù)據(jù)的拷貝次數(shù),提高了文件的操作效率;同時 MMap 只需要提供一段內(nèi)存,只需要關(guān)注往內(nèi)存文件中讀寫操作即可,在操作系統(tǒng)內(nèi)存不足或進程退出時自動寫入文件中;
??????當然,MMap 也有自身的劣勢,因為 MMap 需要提供一度長度的內(nèi)存塊,其映射區(qū)的長度默認是一頁,即 4kb,當存儲的文件內(nèi)容較少時可能會造成空間的浪費;
2.2 Protocol Buffers 編碼結(jié)構(gòu)
??????Protocol Buffers 簡稱 protobuf,是 Google 出品的一種可擴展的序列化數(shù)據(jù)的編碼格式,主要用于通信協(xié)議和數(shù)據(jù)存儲等;利用 varint 原理(一種變長的編碼方式,值越小的數(shù)字,使用的字節(jié)越少)壓縮數(shù)據(jù)以后,二進制數(shù)據(jù)非常緊湊;
??????protobuf 采用了 TLV(TAG-Length-Value) 的編碼格式,減少了分隔符的使用,編碼更為緊湊;

??????protobuf 在更新文件時,雖然也不方便局部更新,但是可以做增量更新,即不管之前是否有相同的 key,一旦有新的數(shù)據(jù)便添加到文件最后,待最終文件讀取時,后面新的數(shù)據(jù)會覆蓋之前老舊的數(shù)據(jù);
??????當添加新的數(shù)據(jù)時文件大小不夠了,需要全量更新,此時需要將 Map 中數(shù)據(jù)按照 MMKV 方式序列化,濾重后保存需要的字節(jié)數(shù),根據(jù)獲取的字節(jié)數(shù)與文件大小進行比較;若保存后的文件大小可以添加新的數(shù)據(jù)時直接添加在最后面,若保存后的文件大小還是不足以添加新的數(shù)據(jù)時,此時需要對 protobuf * 2 擴容;
??????protobuf 功能簡單,作為二進制存儲,可讀性較差;同時無法表示復(fù)雜的概念,通用性相較于 xml 較差;這也是 protobuf 的不足之處;
2.3 flock 文件鎖 + CRC 校驗
??????SharedPreferences 因為線程安全不支持在多進程中進行數(shù)據(jù)更新;而 MMKV 通過 flock 文件鎖和 CRC 校驗支持多進程的讀寫操作;
??????小菜簡單理解,MMKV 在進程 A 中更新了數(shù)據(jù),在進程 B 中獲取當前數(shù)據(jù)時會先通過 CRC 文件校驗看文件是否有過更新,若沒更新直接讀取,若已更新則重新獲取文件內(nèi)容在進行讀??;
??????而為了防止多個進程同時對文件進行寫操作,MMKV 采用了文件鎖 flock 方式來保證同一時間只有一個進程對文件進行寫操作;
3. MMKV 應(yīng)用與注意
??????MMKV 的應(yīng)用非常簡單,根據(jù)官網(wǎng)集成即可:
- Maven 倉庫引入 mmkv;
implementation 'com.tencent:mmkv-static:1.2.2'
- 初始化;
MMKV.initialize(this);
- 根據(jù)文件名稱創(chuàng)建對應(yīng)存儲文件;建議設(shè)置 MMKV 為全局實例,方便統(tǒng)一處理;
// 默認文件名
MMKV kv = MMKV.defaultMMKV();
// 指定文件名
MMKV kv = MMKV.mmkvWithID(Constants.SP_APP_CONFIG);
- 可以通過 encode() 方式存儲數(shù)據(jù)也可以使用和 SharedPreferences 相同的 put() 方式存儲數(shù)據(jù);
kv.encode("name", "阿策小和尚");
kv.encode("age", 18);
kv.putString("address", "北京市海淀區(qū)");
kv.putInt("sex", 0);
- 同樣可以采用 decodeXXX() 或 getXXX() 獲取數(shù)據(jù);
kv.decodeString("name", "");
kv.decodeInt("age", -1);
kv.getString("address", "");
kv.getInt("sex", -1);
- 與 SharedPreferences 一樣,remove() 清除一條數(shù)據(jù),clear() 清空全部數(shù)據(jù);
kv.remove();
kv.clear();
- 對于應(yīng)用中已存在 SharedPreferences 時,MMKV 提供了一鍵轉(zhuǎn)換為 MMKV 方式;
MMKV mmkv = MMKV.mmkvWithID(Constants.SP_APP_CONFIG);
SharedPreferences sp = context.getSharedPreferences(mid, Context.MODE_PRIVATE);
mmkv.importFromSharedPreferences(sp);
sp.edit().clear().commit();

??????小菜對于 SharedPreferences 和 MMKV 的底層源碼還不夠深入,如有錯誤,請多多指導(dǎo)!
來源: 阿策小和尚