SharedPreferences VS MMKV

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

SharedPreferences

1. SharedPreferences 基本介紹

??????SharedPreferences 是一種輕量級(jí)存儲(chǔ)方式,以 key-value 方式存儲(chǔ)在本地 xml 文件中蕊苗;其持久化的本質(zhì)就是在在本地磁盤記錄一個(gè) 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();
    }
}

??????簡(jiǎn)單分析源碼可得,SharedPreferences 只是一個(gè)接口朽砰,SharedPreferencesImpl 為具體的實(shí)現(xiàn)類尖滚,通過(guò) ContextImplgetSharedPreferences() 獲取對(duì)象;

2. SharedPreferences 初始化

SharedPreferences sp = getSharedPreferences(Constants.SP_APP_CONFIG, MODE_PRIVATE);

??????SharedPreferences 的通過(guò) getSharedPreferences() 初始化創(chuàng)建一個(gè)對(duì)象瞧柔;其中 MODE 為文件操作類型漆弄;MODE_PRIVATE 為本應(yīng)用私有的,其他 app 不可訪問(wèn)的造锅;MODE_APPEND 也為應(yīng)用私有撼唾,但是新保存的數(shù)據(jù)放置在文件最后,不會(huì)替換之前已有的 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;
}

??????小菜在源碼處注明了幾個(gè) TAG 需要注意的地方糙箍;

TAG 01: 在根據(jù) name 查詢文件時(shí)渤愁,SharedPreferences 使用了 ArrayMap,相較于 HashMap 更便捷深夯,更節(jié)省空間抖格;

TAG 02: 在創(chuàng)建生成 SharedPreferences 時(shí),通過(guò) cache 來(lái)防止同一個(gè) SharedPreferences 被重復(fù)創(chuàng)建咕晋;

TAG 03: SharedPreferencesImapl 為具體的實(shí)現(xiàn)類雹拄,初始化時(shí)開(kāi)啟新的 I/O 線程讀取整個(gè)文件 startLoadFromDisk(),進(jìn)行 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 為具體的實(shí)現(xiàn)類站辉;putXXX() 編輯后的數(shù)據(jù)保存在 Editor 中呢撞,commit()/apply() 后才會(huì)更新到 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ù)時(shí)根據(jù) mLoaded 文件是否讀取完成判斷饰剥,若未讀取完成 awaitLoadedLocked() 會(huì)被阻塞殊霞,此時(shí)在 UI 主線程中進(jìn)行使用時(shí)就可有可能會(huì)造成 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 通過(guò) commit()apply() 提交更新到 SharedPrefenences汰蓉;兩者的區(qū)別很明顯绷蹲,apply() 通過(guò)線程進(jìn)行異步處理,如果任務(wù)完成則從隊(duì)列中移除 QueuedWork.removeFinisher,無(wú)法獲取提交的結(jié)果祝钢;commit 是同步更新,使用時(shí)會(huì)阻塞主線程拦英,因?yàn)槭峭教峤唬梢垣@取 Boolean 狀態(tài)的提交狀態(tài)疤估,進(jìn)而判斷是否提交成功;

4. SharedPreferences 問(wèn)題與優(yōu)化

??????SharedPreferences 雖因其便利性而應(yīng)用廣泛钞瀑,但也存在一些弊端;

Q1: 編輯 get()/put() 時(shí)均會(huì)涉及到互斥鎖和寫入鎖雕什,并發(fā)操作時(shí)影響性能;
A1: 讀寫操作都是針對(duì)的 SharedPreferences 對(duì)象监徘,可適當(dāng)拆分文件或降低訪問(wèn)頻率等;
Q2: 使用時(shí)出現(xiàn)卡頓引發(fā) GC 或 ANR吧碾;
A2:
  1. 不要存放大數(shù)據(jù)類型的 key-value 避免導(dǎo)致一直在內(nèi)存中無(wú)法釋放;
  2. 盡量避免頻繁讀寫操作倦春;
  3. 盡量減少 apply() 次數(shù),每次都會(huì)新建一個(gè) EditorImpl 對(duì)象睁本,可以批量處理統(tǒng)一提交尿庐;
Q3: 不能跨進(jìn)程通信,不能保證更新本地?cái)?shù)據(jù)后被另一個(gè)進(jìn)程所知呢堰;
A3: 可以借助 ContentProvider 來(lái)在多進(jìn)程中更新數(shù)據(jù)抄瑟;

MMKV

1. MMKV 基本介紹

??????正因?yàn)?SharedPreferences 還有很大的優(yōu)化空間,因?yàn)槲覀儾艜?huì)嘗試其他存儲(chǔ)框架枉疼;其中 騰訊 MMKV 得到很多人的支持皮假;

??????MMKV 分別代表的是 Memory Mapping Key Value,是基于 mmap 內(nèi)存映射的 key-value 組件骂维,底層序列化/反序列化使用 protobuf 實(shí)現(xiàn)惹资,性能高,穩(wěn)定性強(qiáng)航闺;官網(wǎng) Wiki 介紹的優(yōu)勢(shì)很明顯褪测,是目前微信正在使用的輕量級(jí)存儲(chǔ)框架猴誊;在 Android / macOS / Win32 / POSIX 多個(gè)平臺(tái)一并開(kāi)源;

2. MMKV 優(yōu)勢(shì)

??????小菜從如下幾個(gè)角度簡(jiǎn)單分析一下 MMKV 的優(yōu)勢(shì)侮措;

a. 數(shù)據(jù)格式及更新范圍優(yōu)化懈叹;
??????SharedPreferences 采用 xml 數(shù)據(jù)存儲(chǔ),每次讀寫操作都會(huì)全局更新萝毛;MMKV 采用 protobuf 數(shù)據(jù)存儲(chǔ)项阴,更緊密,支持局部更新
b. 文件耗時(shí)操作優(yōu)化笆包;
??????MMKV 采用 MMap 內(nèi)存映射的方式取代 I/O 操作,使用 0拷貝技術(shù)提高更新速度略荡;
c. 跨進(jìn)程狀態(tài)同步庵佣;
??????SharedPreferences 為了線程安全不支持跨進(jìn)程狀態(tài)同步;MMKV 通過(guò) CRC 校驗(yàn) 和文件鎖 flock 實(shí)現(xiàn)跨進(jìn)程狀態(tài)更新汛兜;
d. 應(yīng)用便捷性巴粪,較好的兼容性
??????MMKV 使用方式便捷粥谬,與 SharedPreferences 基本一致肛根,遷移成本低;

2.1 Memory Mapping 內(nèi)存映射

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

??????由此可見(jiàn),MMap 的優(yōu)勢(shì)很明顯了芭届,因?yàn)檫M(jìn)行了內(nèi)存映射褂乍,操作內(nèi)存相當(dāng)于操作文件逃片,無(wú)需開(kāi)啟新的線程褥实,相較于 I/O 對(duì)文件的讀寫操作只需要從磁盤到用戶主存的一次數(shù)據(jù)拷貝過(guò)程性锭,減少了數(shù)據(jù)的拷貝次數(shù),提高了文件的操作效率草冈;同時(shí) MMap 只需要提供一段內(nèi)存怎棱,只需要關(guān)注往內(nèi)存文件中讀寫操作即可拳恋,在操作系統(tǒng)內(nèi)存不足或進(jìn)程退出時(shí)自動(dòng)寫入文件中;

??????當(dāng)然隙赁,MMap 也有自身的劣勢(shì)伞访,因?yàn)?MMap 需要提供一度長(zhǎng)度的內(nèi)存塊轰驳,其映射區(qū)的長(zhǎng)度默認(rèn)是一頁(yè)级解,即 4kb勤哗,當(dāng)存儲(chǔ)的文件內(nèi)容較少時(shí)可能會(huì)造成空間的浪費(fèi);

2.2 Protocol Buffers 編碼結(jié)構(gòu)

??????Protocol Buffers 簡(jiǎn)稱 protobuf豁延,是 Google 出品的一種可擴(kuò)展的序列化數(shù)據(jù)的編碼格式诱咏,主要用于通信協(xié)議和數(shù)據(jù)存儲(chǔ)等袋狞;利用 varint 原理(一種變長(zhǎng)的編碼方式苟鸯,值越小的數(shù)字早处,使用的字節(jié)越少)壓縮數(shù)據(jù)以后砌梆,二進(jìn)制數(shù)據(jù)非常緊湊;

??????protobuf 采用了 TLV(TAG-Length-Value) 的編碼格式咸包,減少了分隔符的使用烂瘫,編碼更為緊湊坟比;

??????protobuf 在更新文件時(shí)葛账,雖然也不方便局部更新注竿,但是可以做增量更新巩割,即不管之前是否有相同的 key宣谈,一旦有新的數(shù)據(jù)便添加到文件最后闻丑,待最終文件讀取時(shí)勋颖,后面新的數(shù)據(jù)會(huì)覆蓋之前老舊的數(shù)據(jù)侥祭;

??????當(dāng)添加新的數(shù)據(jù)時(shí)文件大小不夠了茄厘,需要全量更新,此時(shí)需要將 Map 中數(shù)據(jù)按照 MMKV 方式序列化胎署,濾重后保存需要的字節(jié)數(shù)琼牧,根據(jù)獲取的字節(jié)數(shù)與文件大小進(jìn)行比較;若保存后的文件大小可以添加新的數(shù)據(jù)時(shí)直接添加在最后面滋恬,若保存后的文件大小還是不足以添加新的數(shù)據(jù)時(shí)恢氯,此時(shí)需要對(duì) protobuf * 2 擴(kuò)容勋拟;

??????protobuf 功能簡(jiǎn)單敢靡,作為二進(jìn)制存儲(chǔ)啸胧,可讀性較差纺念;同時(shí)無(wú)法表示復(fù)雜的概念陷谱,通用性相較于 xml 較差烟逊;這也是 protobuf 的不足之處宪躯;

2.3 flock 文件鎖 + CRC 校驗(yàn)

??????SharedPreferences 因?yàn)榫€程安全不支持在多進(jìn)程中進(jìn)行數(shù)據(jù)更新眷唉;而 MMKV 通過(guò) flock 文件鎖和 CRC 校驗(yàn)支持多進(jìn)程的讀寫操作冬阳;

??????小菜簡(jiǎn)單理解肝陪,MMKV 在進(jìn)程 A 中更新了數(shù)據(jù)氯窍,在進(jìn)程 B 中獲取當(dāng)前數(shù)據(jù)時(shí)會(huì)先通過(guò) CRC 文件校驗(yàn)看文件是否有過(guò)更新,若沒(méi)更新直接讀取贝淤,若已更新則重新獲取文件內(nèi)容在進(jìn)行讀炔ゴ稀离陶;

??????而為了防止多個(gè)進(jìn)程同時(shí)對(duì)文件進(jìn)行寫操作招刨,MMKV 采用了文件鎖 flock 方式來(lái)保證同一時(shí)間只有一個(gè)進(jìn)程對(duì)文件進(jìn)行寫操作;

3. MMKV 應(yīng)用與注意

??????MMKV 的應(yīng)用非常簡(jiǎn)單沦寂,根據(jù)官網(wǎng)集成即可:

  1. Maven 倉(cāng)庫(kù)引入 mmkv淘衙;
implementation 'com.tencent:mmkv-static:1.2.2'
  1. 初始化彤守;
MMKV.initialize(this);
  1. 根據(jù)文件名稱創(chuàng)建對(duì)應(yīng)存儲(chǔ)文件;建議設(shè)置 MMKV 為全局實(shí)例试幽,方便統(tǒng)一處理铺坞;
// 默認(rèn)文件名
MMKV kv = MMKV.defaultMMKV();

// 指定文件名
MMKV kv = MMKV.mmkvWithID(Constants.SP_APP_CONFIG);
  1. 可以通過(guò) encode() 方式存儲(chǔ)數(shù)據(jù)也可以使用和 SharedPreferences 相同的 put() 方式存儲(chǔ)數(shù)據(jù)济榨;
kv.encode("name", "阿策小和尚");
kv.encode("age", 18);

kv.putString("address", "北京市海淀區(qū)");
kv.putInt("sex", 0);
  1. 同樣可以采用 decodeXXX()getXXX() 獲取數(shù)據(jù)腐晾;
kv.decodeString("name", "");
kv.decodeInt("age", -1);

kv.getString("address", "");
kv.getInt("sex", -1);
  1. SharedPreferences 一樣藻糖,remove() 清除一條數(shù)據(jù)淹冰,clear() 清空全部數(shù)據(jù);
kv.remove();

kv.clear();
  1. 對(duì)于應(yīng)用中已存在 SharedPreferences 時(shí)巨柒,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();

??????小菜對(duì)于 SharedPreferencesMMKV 的底層源碼還不夠深入,如有錯(cuò)誤潘拱,請(qǐng)多多指導(dǎo)疹鳄!

來(lái)源: 阿策小和尚

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市芦岂,隨后出現(xiàn)的幾起案子瘪弓,更是在濱河造成了極大的恐慌,老刑警劉巖禽最,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件懦趋,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門憎夷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)臼予,“玉大人,你說(shuō)我怎么就攤上這事械哟”铮” “怎么了钧忽?”我有些...
    開(kāi)封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng)纽乱,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任镜粤,我火速辦了婚禮同规,結(jié)果婚禮上朱灿,老公的妹妹穿的比我還像新娘侣灶。我一直安慰自己凡怎,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著左胞,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上薇缅,一...
    開(kāi)封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天浮毯,我揣著相機(jī)與錄音演痒,去河邊找鬼蹦锋。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播趟大,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼铁坎!你這毒婦竟也來(lái)了袁勺?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤畜普,失蹤者是張志新(化名)和其女友劉穎期丰,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體吃挑,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡钝荡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了儒鹿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片化撕。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖约炎,靈堂內(nèi)的尸體忽然破棺而出植阴,到底是詐尸還是另有隱情,我是刑警寧澤圾浅,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布掠手,位于F島的核電站,受9級(jí)特大地震影響狸捕,放射性物質(zhì)發(fā)生泄漏喷鸽。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一灸拍、第九天 我趴在偏房一處隱蔽的房頂上張望做祝。 院中可真熱鬧,春花似錦鸡岗、人聲如沸混槐。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)声登。三九已至,卻和暖如春揣苏,著一層夾襖步出監(jiān)牢的瞬間悯嗓,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工卸察, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留脯厨,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓坑质,卻偏偏與公主長(zhǎng)得像合武,于是被迫代替她去往敵國(guó)和親个少。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345