MMKV 高性能的數(shù)據(jù)存取框架解讀

MMKV

目標

了解MMKV

MMKV的基本應用

MMKV的原理概念

多進程設(shè)計思想

性能對比

源碼解讀

簡介

MMKV 是基于 mmap 內(nèi)存映射的 key-value 組件普气,底層序列化/反序列化使用 protobuf 實現(xiàn),性能高抹恳,穩(wěn)定性強锡搜。

官方文檔:https://github.com/Tencent/MMKV/blob/master/README_CN.md

項目地址:https://github.com/Tencent/MMKV

mmap

簡單解釋(僅供參考)

把文件描述符fd(部分硬件資源外存統(tǒng)一描述符)映射到虛擬空間中橙困,所以能夠?qū)崿F(xiàn)進程間的通信瞧掺、數(shù)據(jù)存取耕餐。


image-20210819112108803.png

映射流程(僅供參考)

1、用戶進程調(diào)用內(nèi)存映射函數(shù)庫mmap辟狈,當前進程在虛擬地址空間中肠缔,尋找一段空閑的滿足要求的虛擬地址

2哼转、此時內(nèi)核收到相關(guān)請求后會調(diào)用內(nèi)核的mmap函數(shù)明未,注意,不同于用戶空間庫函數(shù)壹蔓。內(nèi)核mmap函數(shù)通過虛擬文件系統(tǒng)定位到文件磁盤物理地址趟妥,既實現(xiàn)了文件地址和虛擬地址區(qū)域的映射關(guān)系。 此時佣蓉,這片虛擬地址并沒有任何數(shù)據(jù)關(guān)聯(lián)到主存中披摄。

注意亲雪,前兩個階段僅在于創(chuàng)建虛擬區(qū)間并完成地址映射,但是并沒有將任何文件數(shù)據(jù)的拷貝至主存疚膊。真正的文件讀取是當進程發(fā)起讀或?qū)懖僮鲿r义辕。

3、進程的讀或?qū)懖僮髟L問虛擬地址空間這一段映射地址寓盗,現(xiàn)這一段地址并不在物理頁面上灌砖。因為目前只建立了地址映射,真正的硬盤數(shù)據(jù)還沒有拷貝到內(nèi)存中傀蚌,因此引發(fā)缺頁中斷基显。

4、由于引發(fā)了缺頁中斷善炫,內(nèi)核則調(diào)用nopage函數(shù)把所缺的頁從磁盤裝入到主存中续镇。

5、之后用戶進程即可對這片主存進行讀或者寫的操作销部,如果寫操作改變了其內(nèi)容摸航,一定時間后系統(tǒng)會自動回寫臟頁面到對應磁盤地址,也即完成了寫入到文件的過程舅桩。

應用

Linux進程的創(chuàng)建

Android Binder

微信MMKV組件

美團Logan

參考文章

Android-內(nèi)存映射mmap

mmap的理解

Android應用使用mmap實例

ProtoBuf

簡介

protocol buffers 是一種語言無關(guān)酱虎、平臺無關(guān)、可擴展的序列化結(jié)構(gòu)數(shù)據(jù)的方法擂涛,它可用于(數(shù)據(jù))通信協(xié)議读串、數(shù)據(jù)存儲等。

更多內(nèi)容撒妈、實際應用可參考官方文檔恢暖。

官方文檔:https://developers.google.com/protocol-buffers/docs/overview

特性

語言無關(guān)、平臺無關(guān):即 ProtoBuf 支持 Java狰右、C++杰捂、Python 等多種語言,支持多個平臺

高效:即比 XML 更衅灏觥(3 ~ 10倍)嫁佳、更快(20 ~ 100倍)、更為簡單

擴展性谷暮、兼容性好:你可以更新數(shù)據(jù)結(jié)構(gòu)蒿往,而不影響和破壞原有的舊程序

數(shù)據(jù)結(jié)構(gòu)

20191225165541255.png

時間效率對比:

數(shù)據(jù)格式 1000條數(shù)據(jù) 5000條數(shù)據(jù)
Protobuf 195ms 647ms
Json 515ms 2293ms

空間效率對比:

數(shù)據(jù)格式 5000條數(shù)據(jù)
Protobuf 22MB
Json 29MB

參考文章

http://www.reibang.com/p/73c9ed3a4877
http://www.reibang.com/p/a24c88c0526a

簡單使用

MMKV 的使用非常簡單,所有變更立馬生效湿弦,無需調(diào)用 sync瓤漏、apply

依賴

dependencies {
    implementation 'com.tencent:mmkv:1.0.10'
    // replace "1.0.10" with any available version
}

初始化

配置 MMKV 根目錄

在 App 啟動時初始化 MMKV,設(shè)定 MMKV 的根目錄(files/mmkv/)蔬充,例如在 Application 里:

public void onCreate() {
    super.onCreate();

    String rootDir = MMKV.initialize(this);
    System.out.println("mmkv root: " + rootDir);
     //data/user/0包名/files/mmkv
}

其他初始化的方法

//指定日志級別
initialize(Context context, MMKVLogLevel logLevel)
//指定存儲地址和日志級別
initialize(String rootDir)
initialize(String rootDir, MMKVLogLevel logLevel)
//MMKV.LibLoader用來解決Android 設(shè)備(API level 19)在安裝/更新 APK 時出錯問題
initialize(String rootDir, MMKV.LibLoader loader)
initialize(String rootDir, MMKV.LibLoader loader, MMKVLogLevel logLevel)

CRUD 操作

MMKV 提供一個全局的實例俯在,可以直接使用:

import com.tencent.mmkv.MMKV;
//……

MMKV kv = MMKV.defaultMMKV();

kv.encode("bool", true);
boolean bValue = kv.decodeBool("bool");

kv.encode("int", Integer.MIN_VALUE);
int iValue = kv.decodeInt("int");

kv.encode("string", "Hello from mmkv");
String str = kv.decodeString("string");

刪除 & 查詢

MMKV kv = MMKV.defaultMMKV();

kv.removeValueForKey("bool");
System.out.println("bool: " + kv.decodeBool("bool"));
    
kv.removeValuesForKeys(new String[]{"int", "long"});
System.out.println("allKeys: " + Arrays.toString(kv.allKeys()));

boolean hasBool = kv.containsKey("bool");

區(qū)分存儲

使用MMKV.mmkvWithID即可創(chuàng)建不同的存儲區(qū)域的MMKV實例。

MMKV kv = MMKV.mmkvWithID("MyID");
kv.encode("bool", true);

支持的數(shù)據(jù)類型

  • 支持以下 Java 語言基礎(chǔ)類型:

    • boolean娃惯、int跷乐、long、float趾浅、double愕提、byte[]
  • 支持以下 Java 類和容器:

    • String、Set<String>
    • 任何實現(xiàn)了Parcelable的類型

SharedPreferences 遷移

  • MMKV 提供了 importFromSharedPreferences() 函數(shù)皿哨,可以比較方便地遷移數(shù)據(jù)過來
/**
 * An highly efficient, reliable, multi-process key-value storage framework.
 * THE PERFECT drop-in replacement for SharedPreferences and MultiProcessSharedPreferences.
 */
public class MMKV implements SharedPreferences, SharedPreferences.Editor {
  • MKV 還額外實現(xiàn)了一遍 SharedPreferences浅侨、SharedPreferences.Editor 這兩個 interface,在遷移的時候只需兩三行代碼即可证膨,其他 CRUD 操作代碼都不用改如输。
private void testImportSharedPreferences() {
    //SharedPreferences preferences = getSharedPreferences("myData", MODE_PRIVATE);
    MMKV preferences = MMKV.mmkvWithID("myData");
    // 遷移舊數(shù)據(jù)
    {
        SharedPreferences old_man = getSharedPreferences("myData", MODE_PRIVATE);
        preferences.importFromSharedPreferences(old_man);
        old_man.edit().clear().commit();
    }
    // 跟以前用法一樣
    SharedPreferences.Editor editor = preferences.edit();
    editor.putBoolean("bool", true);
    editor.putInt("int", Integer.MIN_VALUE);
    editor.putLong("long", Long.MAX_VALUE);
    editor.putFloat("float", -3.14f);
    editor.putString("string", "hello, imported");
    HashSet<String> set = new HashSet<String>();
    set.add("W"); set.add("e"); set.add("C"); set.add("h"); set.add("a"); set.add("t");
    editor.putStringSet("string-set", set);
    // 無需調(diào)用 commit()
    //editor.commit();
}

進階使用

日志

日志切面AOP思想

MMKV 默認將日志打印到 logcat,不便于對線上問題進行定位和解決央勒。你可以在 App 啟動時接收轉(zhuǎn)發(fā) MMKV 的日志不见。實現(xiàn)MMKVHandler接口,添加類似下面的代碼:

@Override
public boolean wantLogRedirecting() {
    return true;
}

@Override
public void mmkvLog(MMKVLogLevel level, String file, int line, String func, String message) {
    String log = "<" + file + ":" + line + "::" + func + "> " + message;
    switch (level) {
        case LevelDebug:
            //Log.d("redirect logging MMKV", log);
            break;
        case LevelInfo:
            //Log.i("redirect logging MMKV", log);
            break;
        case LevelWarning:
            //Log.w("redirect logging MMKV", log);
            break;
        case LevelError:
            //Log.e("redirect logging MMKV", log);
            break;
        case LevelNone:
            //Log.e("redirect logging MMKV", log);
            break;
    }
}

如果你不希望 MMKV 打印日志崔步,你可以關(guān)掉它(雖然我們強烈不建議你這么做)稳吮。
注意:除非有非常強烈的證據(jù)表明MMKV的日志拖慢了App的速度,你不應該關(guān)掉日志井濒。沒有日志灶似,日后萬一用戶有問題,將無法跟進瑞你。

MMKV.setLogLevel(MMKVLogLevel.LevelNone);

加密

MMKV 默認明文存儲所有 key-value酪惭,依賴 Android 系統(tǒng)的沙盒機制保證文件加密。如果你擔心信息泄露者甲,你可以選擇加密 MMKV春感。

String cryptKey = "My-Encrypt-Key";
MMKV kv = MMKV.mmkvWithID("MyID", MMKV.SINGLE_PROCESS_MODE, cryptKey);

你可以更改密鑰,也可以將一個加密 MMKV 改成明文过牙,或者反過來甥厦。

final String mmapID = "testAES_reKey1";
// an unencrypted MMKV instance
MMKV kv = MMKV.mmkvWithID(mmapID, MMKV.SINGLE_PROCESS_MODE, null);

// change from unencrypted to encrypted
kv.reKey("Key_seq_1");

// change encryption key
kv.reKey("Key_seq_2");

// change from encrypted to unencrypted
kv.reKey(null);

自定義 library loader

一些 Android 設(shè)備(API level 19)在安裝/更新 APK 時可能出錯, 導致 libmmkv.so 找不到。然后就會遇到 java.lang.UnsatisfiedLinkError 之類的 crash寇钉。有個開源庫 ReLinker 專門解決這個問題,你可以用它來加載 MMKV:

String dir = getFilesDir().getAbsolutePath() + "/mmkv";
MMKV.initialize(dir, new MMKV.LibLoader() {
    @Override
    public void loadLibrary(String libName) {
        ReLinker.loadLibrary(MyApplication.this, libName);
    }
});

Relinker簡介:

本地庫加載框架舶赔,github1000+的star

原理:

嘗試使用系統(tǒng)原生方式去加載so扫倡,如果加載失敗,Relinker會嘗試從apk中拷貝so到App沙箱目錄下,然后再去嘗試加載so撵溃。最終疚鲤,我們可以使用 ReLinker.loadLibrary(context, “mylibrary”) 來加載本地庫。

Native Buffer

當從 MMKV 取一個 String or byte[]的時候缘挑,會有一次從 native 到 JVM 的內(nèi)存拷貝集歇。如果這個值立即傳遞到另一個 native 庫(JNI),又會有一次從 JVM 到 native 的內(nèi)存拷貝语淘。當這個值比較大的時候诲宇,整個過程會非常浪費。Native Buffer 就是為了解決這個問題惶翻。
Native Buffer 是由 native 創(chuàng)建的內(nèi)存緩沖區(qū)姑蓝,在 Java 里封裝成 NativeBuffer 類型,可以透明傳遞到另一個 native 庫進行訪問處理吕粗。整個過程避免了先拷內(nèi)存到 JVM 又從 JVM 拷回來導致的浪費纺荧。示例代碼:

int sizeNeeded = kv.getValueActualSize("bytes");
NativeBuffer nativeBuffer = MMKV.createNativeBuffer(sizeNeeded);
if (nativeBuffer != null) {
    int size = kv.writeValueToNativeBuffer("bytes", nativeBuffer);
    Log.i("MMKV", "size Needed = " + sizeNeeded + " written size = " + size);

    // pass nativeBuffer to another native library
    // ...

    // destroy when you're done
    MMKV.destroyNativeBuffer(nativeBuffer);
}

跨進程通信的實現(xiàn)

本質(zhì):共享MMKV實例化信息完成對象的偽復制

  • 通信的數(shù)據(jù)對象

    該類MMKV內(nèi)部已經(jīng)實現(xiàn),傳遞進程A的mmkv信息給B進程颅筋,B進程新建MMKV實例宙暇,B就可以通過MMKV實例來完成數(shù)據(jù)的操作

public final class ParcelableMMKV implements Parcelable {
    private final String mmapID;
    private int ashmemFD = -1;
    private int ashmemMetaFD = -1;
    private String cryptKey = null;

    public ParcelableMMKV(MMKV mmkv) {
        mmapID = mmkv.mmapID();
        ashmemFD = mmkv.ashmemFD();
        ashmemMetaFD = mmkv.ashmemMetaFD();
        cryptKey = mmkv.cryptKey();
    }

    private ParcelableMMKV(String id, int fd, int metaFD, String key) {
        mmapID = id;
        ashmemFD = fd;
        ashmemMetaFD = metaFD;
        cryptKey = key;
    }

    public MMKV toMMKV() {
        if (ashmemFD >= 0 && ashmemMetaFD >= 0) {
            return MMKV.mmkvWithAshmemFD(mmapID, ashmemFD, ashmemMetaFD, cryptKey);
        }
        return null;
    }
}
  • Aidl文件,需要手動創(chuàng)建該文件

    import com.tencent.mmkv.ParcelableMMKV;
    
    interface IAshmemMMKV {
        ParcelableMMKV GetAshmemMMKV();
    }
    

    Aidl定義了跨進程通信的方法細則议泵,這里只需要一個get方法客给,返回ParcelableMMKV通信實體。

  • 服務(wù)端

    服務(wù)端Service

    public class UserServer extends Service {
      @Nullable
        @Override
        public IBinder onBind(Intent intent) {
              Log.i(TAG, "onBind, intent=" + intent);
            return new AshmemMMKVGetter();
        }
    
    }
    
    public class AshmemMMKVGetter extends IAshmemMMKV.Stub {
    
        private AshmemMMKVGetter() {
            // 1M, ashmem cannot change size after opened
            final String id = "tetAshmemMMKV";
            try {
                m_ashmemMMKV = MMKV.mmkvWithAshmemID(BenchMarkBaseService.this, id, AshmemMMKV_Size,
                        MMKV.MULTI_PROCESS_MODE, CryptKey);
                m_ashmemMMKV.encode("bool", true);
            } catch (Exception e) {
                Log.e("MMKV", e.getMessage());
            }
        }
    
        public ParcelableMMKV GetAshmemMMKV() {
            return new ParcelableMMKV(m_ashmemMMKV);
        }
    }
    
  • 客戶端

    onServiceConnected連接之后

Intent intent = new Intent();
intent.setAction("***.***.***");
intent.setPackage("***.***.***");
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);

 private ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
               IAshmemMMKV ashmemMMKV = IAshmemMMKV.Stub.asInterface(service);
        try {
            ParcelableMMKV parcelableMMKV = ashmemMMKV.GetAshmemMMKV();
            if (parcelableMMKV != null) {
                m_ashmemMMKV = parcelableMMKV.toMMKV();
                if (m_ashmemMMKV != null) {
                    Log.i("MMKV", "ashmem bool: " + m_ashmemMMKV.decodeBool("bool"));
                }
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            isBind = false;
          
        }
    }; 

原理

內(nèi)存準備

通過 mmap 內(nèi)存映射文件肢簿,提供一段可供隨時寫入的內(nèi)存塊靶剑,App 只管往里面寫數(shù)據(jù),由操作系統(tǒng)負責將內(nèi)存回寫到文件池充,不必擔心 crash 導致數(shù)據(jù)丟失桩引。

數(shù)據(jù)組織

數(shù)據(jù)序列化方面我們選用 protobuf 協(xié)議,pb 在性能和空間占用上都有不錯的表現(xiàn)收夸】咏常考慮到我們要提供的是通用 kv 組件,key 可以限定是 string 字符串類型卧惜,value 則多種多樣(int/bool/double 等)厘灼。要做到通用的話,考慮將 value 通過 protobuf 協(xié)議序列化成統(tǒng)一的內(nèi)存塊(buffer)咽瓷,然后就可以將這些 KV 對象序列化到內(nèi)存中设凹。

message KV {
    string key = 1;
    buffer value = 2;
}

-(BOOL)setInt32:(int32_t)value forKey:(NSString*)key {
    auto data = PBEncode(value);
    return [self setData:data forKey:key];
}

-(BOOL)setData:(NSData*)data forKey:(NSString*)key {
    auto kv = KV { key, data };
    auto buf = PBEncode(kv);
    return [self write:buf];
}

寫入優(yōu)化

標準 protobuf 不提供增量更新的能力,每次寫入都必須全量寫入茅姜∩林欤考慮到主要使用場景是頻繁地進行寫入更新,我們需要有增量更新的能力:將增量 kv 對象序列化后,直接 append 到內(nèi)存末尾奋姿;這樣同一個 key 會有新舊若干份數(shù)據(jù)锄开,最新的數(shù)據(jù)在最后;那么只需在程序啟動第一次打開 mmkv 時称诗,不斷用后讀入的 value 替換之前的值萍悴,就可以保證數(shù)據(jù)是最新有效的。

空間增長

使用 append 實現(xiàn)增量更新帶來了一個新的問題寓免,就是不斷 append 的話癣诱,文件大小會增長得不可控。例如同一個 key 不斷更新的話再榄,是可能耗盡幾百 M 甚至上 G 空間狡刘,而事實上整個 kv 文件就這一個 key,不到 1k 空間就存得下困鸥。這明顯是不可取的嗅蔬。我們需要在性能和空間上做個折中:以內(nèi)存 pagesize 為單位申請空間,在空間用盡之前都是 append 模式疾就;當 append 到文件末尾時澜术,進行文件重整、key 排重猬腰,嘗試序列化保存排重結(jié)果鸟废;排重后空間還是不夠用的話,將文件擴大一倍姑荷,直到空間足夠盒延。

-(BOOL)append:(NSData*)data {
    if (space >= data.length) {
        append(fd, data);
    } else {
        newData = unique(m_allKV);
        if (total_space >= newData.length) {
            write(fd, newData);
        } else {
            while (total_space < newData.length) {
                total_space *= 2;
            }
            ftruncate(fd, total_space);
            write(fd, newData);
        }
    }
}

數(shù)據(jù)有效性

考慮到文件系統(tǒng)、操作系統(tǒng)都有一定的不穩(wěn)定性鼠冕,我們另外增加了 crc 校驗添寺,對無效數(shù)據(jù)進行甄別。在 iOS 微信現(xiàn)網(wǎng)環(huán)境上懈费,我們觀察到有平均約 70萬日次的數(shù)據(jù)校驗不通過计露。

多進程設(shè)計思想

官網(wǎng)地址:https://github.com/Tencent/MMKV/wiki/android_ipc

官網(wǎng)有詳細的說明,這里主要分享思想:

CS架構(gòu):

IPC CS架構(gòu)有Binder憎乙、Socket等票罐,特點是一個單獨進程管理數(shù)據(jù),數(shù)據(jù)同步不易出錯泞边,簡單好用易上手该押,缺點是慢。

去中心化:

只需要將文件 mmap 到每個訪問進程的內(nèi)存空間繁堡,加上合適的進程鎖沈善,再處理好數(shù)據(jù)的同步乡数,就能夠?qū)崿F(xiàn)多進程并發(fā)訪問椭蹄。

性能對比

單進程

讀寫效率

mmkv SharedPreferences sqlite
write int 1000 6.5 693.1 774.4
write String 1000 18.9 1003.9 857.3
read int 1000 4.3 1.5 302.9
read String 1000 8.3 1.3 320.7

單進程性能
可見闻牡,MMKV 在寫入性能上遠遠超越 SharedPreferences & SQLite,在讀取性能上也有相近或超越的表現(xiàn)绳矩。

image-20210826111619059.png

(測試機器是 華為 Mate 20 Pro 128G罩润,Android 10,每組操作重復 1k 次翼馆,時間單位是 ms割以。)

多進程性能

可見,MMKV 無論是在寫入性能還是在讀取性能应媚,都遠遠超越 MultiProcessSharedPreferences & SQLite & SQLite严沥。

image-20210826111725683.png

性能對比: https://github.com/Tencent/MMKV/wiki/android_benchmark_cn

原理上和SharedPreference區(qū)別

SharedPreference原理

本質(zhì)是在本地磁盤記錄了一個xml文件,在構(gòu)造方法中開啟一個子線程加載磁盤中的xml文件

@UnsupportedAppUsage
private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}

SharedPreferencesImpl內(nèi)部維護Map緩存中姜,所以SharedPreference讀的效率很高消玄,但是寫得時候都是通過FileOutputStreame文件IO得方式完成數(shù)據(jù)更新操作。


20191224163605643.png

MMKV

利用mmap完成數(shù)據(jù)的讀寫丢胚,讀寫高效翩瓜。

SharedPreference MMKV
讀寫方式 IO mmap
數(shù)據(jù)格式 XML 總體結(jié)構(gòu)、整型編碼携龟、二進制
更新方式 全量更新 增量與全量寫入

SharedPreferences注意點

  1. 只要file name相同睦柴,拿到的就是同一個SharedPreferencesImpl對象围肥,內(nèi)部有緩存機制,首次獲取才會創(chuàng)建對象。
  2. 在SharedPreferencesImpl構(gòu)造方法中躬络,會開啟子線程把對應的文件key-value全部加載進內(nèi)存,加載結(jié)束后盯捌,mLoaded被設(shè)置為true拔莱。
  3. 調(diào)用getXXX方法時,會阻塞等待直到mLoaded為true匿又,也就是getXXX方法是有可能阻塞UI線程的方灾,另外,調(diào)用contains和 edit等方法也是碌更。
  4. 寫數(shù)據(jù)時裕偿,會先拿到一個EditorImpl對象,然后putXXX痛单,這時只是把數(shù)據(jù)寫入到內(nèi)存中嘿棘,最后調(diào)用commit或者apply方法,才會真正寫入文件旭绒。
  5. 不管是commit還是apply方法鸟妙,第一步都是調(diào)用commitToMemory方法生成一個MemoryCommitResult對象焦人,注意這里會先處理clear舊的key-value,再處理新添加的key-value重父,另外value為this或者null都表示需要被remove掉花椭。
  6. 調(diào)用commit方法,就會同步執(zhí)行寫入文件的操作房午,該方法是耗時操作矿辽,不能在主線程中調(diào)用,該方法最后會返回成功或失敗結(jié)果郭厌。
  7. 調(diào)用apply方法袋倔,就會把任務(wù)放到QueuedWork的隊列中,然后在HandlerThread中執(zhí)行折柠,然后apply方法會立即返回宾娜。但如果是Android8.0之前,這里就是放到QueuedWork的一個單線程中執(zhí)行了扇售。
  8. 最后是寫入文件前塔,會先把原有的文件命名為bak備份文件,然后創(chuàng)建新的文件全量寫入缘眶,寫入成功后嘱根,把bak備份文件刪除掉。

安全

基于Android的沙盒模式巷懈,在內(nèi)存讀寫的方式上做了改變该抒,所以不存在應用程序之前的安全問題。

MMKV使用ProtoBuf 編碼顶燕,另外增加了內(nèi)部實現(xiàn)的加密模式(AES CFB)凑保,相比SharedPrefrence,在文件暴露的情況下MMKV的數(shù)據(jù)不具有可讀性涌攻。

在TV中的應用

配置參數(shù)較多欧引、需要頻繁讀寫修改參數(shù)的場景

可以提高讀寫耗時,減少SP帶來的耗時成本和操作不當引發(fā)的ANR

源碼解讀

初始化

 public static String initialize(Context context) {
        String root = context.getFilesDir().getAbsolutePath() + "/mmkv";
        MMKVLogLevel logLevel = MMKVLogLevel.LevelInfo;
        return initialize(root, (MMKV.LibLoader)null, logLevel);
    }

    public static String initialize(String rootDir, LibLoader loader, MMKVLogLevel logLevel) {
        if (loader != null) {
            if (BuildConfig.FLAVOR.equals("SharedCpp")) {
                loader.loadLibrary("c++_shared");
            }
            loader.loadLibrary("mmkv");
        } else {
            if (BuildConfig.FLAVOR.equals("SharedCpp")) {
                System.loadLibrary("c++_shared");
            }
            System.loadLibrary("mmkv");
        }
        MMKV.rootDir = rootDir;
        jniInitialize(MMKV.rootDir, logLevel2Int(logLevel));
        return rootDir;
    }

1.當不指定目錄的時候恳谎,會創(chuàng)建一個app內(nèi)的/data/data/包名/files/mmkv的目錄芝此。所有的文件都保存在里面;

2.加載兩個so庫因痛,c++_shared以及mmkv婚苹, 根據(jù)打包配置來選擇是否要加載c++_shared

native_bridge.cpp
MMKV_JNI void jniInitialize(JNIEnv *env, jobject obj, jstring rootDir, jint logLevel) {
    if (!rootDir) {
        return;
    }
    const char *kstr = env->GetStringUTFChars(rootDir, nullptr);
    if (kstr) {
        //獲取rootDir的url char指針數(shù)組字符串,調(diào)用MMKV::initializeMMKV進一步初始化鸵膏。
        MMKV::initializeMMKV(kstr, (MMKVLogLevel) logLevel);
        env->ReleaseStringUTFChars(rootDir, kstr);
    }
}

MMKV.cpp
void initialize() {
    //創(chuàng)建了MMKV實例的散列表
    g_instanceDic = new unordered_map<string, MMKV *>;
    g_instanceLock = new ThreadLock();
    g_instanceLock->initialize();

    mmkv::DEFAULT_MMAP_SIZE = mmkv::getPageSize();
    MMKVInfo("page size:%d", DEFAULT_MMAP_SIZE);
}

ThreadOnceToken_t once_control = ThreadOnceUninitialized;

void MMKV::initializeMMKV(const MMKVPath_t &rootDir, MMKVLogLevel logLevel) {
    g_currentLogLevel = logLevel;
    //初始化全局的線程鎖ThreadLock
    ThreadLock::ThreadOnce(&once_control, initialize);

    g_rootDir = rootDir;
    //創(chuàng)建文件夾
    mkPath(g_rootDir);

    MMKVInfo("root dir: " MMKV_PATH_FORMAT, g_rootDir.c_str());
}

MMKV 的實例化

java層的實例化

defaultMMKV

 public static MMKV defaultMMKV() {
        if (rootDir == null) {
            throw new IllegalStateException("You should Call MMKV.initialize() first.");
        }

        long handle = getDefaultMMKV(SINGLE_PROCESS_MODE, null);
        return new MMKV(handle);
    }

//構(gòu)造函數(shù)
private MMKV(long handle) {
    nativeHandle = handle;
}

getDefaultMMKV Native層做好實例化工作返回一個long類型的handle膊升,以這個handler作為Java層MMKV的構(gòu)造參數(shù)

mmkvWithID

與defaultMMKV區(qū)別就是多了參數(shù)設(shè)置

public static MMKV mmkvWithID(String mmapID) {
        if (rootDir == null) {
            throw new IllegalStateException("You should Call MMKV.initialize() first.");
        }

        long handle = getMMKVWithID(mmapID, SINGLE_PROCESS_MODE, null, null);
        return new MMKV(handle);
    }

native層實例化

native-bridge.cpp==>getDefaultMMKV

MMKV.cpp==>mmkvWithID 默認的ID為mmkv.default

native-bridge.cpp
MMKV_JNI jlong getDefaultMMKV(JNIEnv *env, jobject obj, jint mode, jstring cryptKey) {
    MMKV *kv = nullptr;

    if (cryptKey) {
        string crypt = jstring2string(env, cryptKey);
        if (crypt.length() > 0) {
            kv = MMKV::defaultMMKV((MMKVMode) mode, &crypt);
        }
    }
    if (!kv) {
        kv = MMKV::defaultMMKV((MMKVMode) mode, nullptr);
    }

    return (jlong) kv;
}
MMKV.cpp

#define DEFAULT_MMAP_ID "mmkv.default"
MMKV *MMKV::defaultMMKV(MMKVMode mode, string *cryptKey) {
#ifndef MMKV_ANDROID
    return mmkvWithID(DEFAULT_MMAP_ID, mode, cryptKey);
#else
    return mmkvWithID(DEFAULT_MMAP_ID, DEFAULT_MMAP_SIZE, mode, cryptKey);
#endif
}

MMKV.h
static MMKV *mmkvWithID(const std::string &mmapID,
                            int size = mmkv::DEFAULT_MMAP_SIZE,
                            MMKVMode mode = MMKV_SINGLE_PROCESS,
                            std::string *cryptKey = nullptr,
                            MMKVPath_t *relativePath = nullptr);

只要是實例化,最后都是調(diào)用mmkvWithID進行實例化谭企。默認的mmkv的id就是mmkv.default

mmkvWithID

MMKV.cpp
MMKV *MMKV::mmkvWithID(const string &mmapID, int size, MMKVMode mode, string *cryptKey, string *relativePath) {

    if (mmapID.empty()) {
        return nullptr;
    }
    SCOPED_LOCK(g_instanceLock);
    //取 mmapID relativePath MMKV_PATH_SLASH 的 md5值作為key
    auto mmapKey = mmapedKVKey(mmapID, relativePath);
    auto itr = g_instanceDic->find(mmapKey);
    if (itr != g_instanceDic->end()) {
        MMKV *kv = itr->second;
        return kv;
    }
    if (relativePath) {
        if (!isFileExist(*relativePath)) {
            if (!mkPath(*relativePath)) {
                return nullptr;
            }
        }
        MMKVInfo("prepare to load %s (id %s) from relativePath %s", mmapID.c_str(), mmapKey.c_str(),
                 relativePath->c_str());
    }
    //實例化
    auto kv = new MMKV(mmapID, size, mode, cryptKey, relativePath);
    (*g_instanceDic)[mmapKey] = kv;
    return kv;
}

將所有的MMKV實例都會保存在之前實例化的g_instanceDic散列表中廓译。其中mmkv每一個id對應一個文件的路徑:

  • 相對路徑(android中是 data/data/包名/files/mmkv) + / + mmkvID

如果發(fā)現(xiàn)對應路徑下的mmkv在散列表中已經(jīng)緩存了评肆,則直接返回。否則就會把相對路徑保存下來非区,傳遞給MMKV進行實例化瓜挽,并保存在g_instanceDic散列表中。

MMKV 的構(gòu)造函數(shù)

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))
    , m_crcPath(crcPathWithID(m_mmapID, mode, relativePath))
    , 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_crypter = new AESCrypt(cryptKey->data(), cryptKey->length());
    }

    m_needLoadFromFile = true;
    m_hasFullWriteback = false;

    m_crcDigest = 0;

    m_sharedProcessLock->m_enable = m_isInterProcess;
    m_exclusiveProcessLock->m_enable = m_isInterProcess;

    // sensitive zone
    {
        SCOPED_LOCK(m_sharedProcessLock);
        loadFromFile();
    }
}
  • 1.m_mmapID MMKV的ID通過mmapedKVKey創(chuàng)建:
string mmapedKVKey(const string &mmapID, MMKVPath_t *relativePath) {
    if (relativePath && g_rootDir != (*relativePath)) {
        return md5(*relativePath + MMKV_PATH_SLASH + string2MMKVPath_t(mmapID));
    }
    return mmapID;
}

mmkvID就是經(jīng)過md5后對應緩存文件對應的路徑院仿。

  • 2.m_path mmkv 緩存的路徑通過mappedKVPathWithID生成
MMKVPath_t mappedKVPathWithID(const string &mmapID, MMKVMode mode, MMKVPath_t *relativePath) {
#ifndef MMKV_ANDROID
...
#else
    if (mode & MMKV_ASHMEM) {
        return ashmemMMKVPathWithID(encodeFilePath(mmapID));
    } else if (relativePath) {
#endif
        return *relativePath + MMKV_PATH_SLASH + encodeFilePath(mmapID);
    }
    return g_rootDir + MMKV_PATH_SLASH + encodeFilePath(mmapID);
}

能看到這里是根據(jù)當前的mode初始化id秸抚,如果不是ashmem匿名共享內(nèi)存模式進行創(chuàng)建速和,則會和上面的處理類似歹垫。id就是經(jīng)過md5后對應緩存文件對應的路徑。

注意這里mode設(shè)置的是MMKV_ASHMEM颠放,也就是ashmem匿名共享內(nèi)存模式則是如下創(chuàng)建方法:

constexpr char ASHMEM_NAME_DEF[] = "/dev/ashmem";

MMKVPath_t ashmemMMKVPathWithID(const MMKVPath_t &mmapID) {
    return MMKVPath_t(ASHMEM_NAME_DEF) + MMKV_PATH_SLASH + mmapID;
}

實際上就是在驅(qū)動目錄下的一個內(nèi)存文件地址排惨。

  • 3.m_crcPath 一個.crc文件的路徑。這個crc文件實際上用于保存crc數(shù)據(jù)校驗key碰凶,避免出現(xiàn)傳輸異常的數(shù)據(jù)進行保存了暮芭。
  • 4.m_file 一個依據(jù)m_path構(gòu)建的內(nèi)存文件MemoryFile對象。
  • 5.m_metaFile 一個依據(jù)m_crcPath構(gòu)建的內(nèi)存文件MemoryFile對象欲低。
  • 6.m_metaInfo 一個MMKVMetaInfo結(jié)構(gòu)體辕宏,這個結(jié)構(gòu)體一般是讀寫的時候,帶上的MMKV的版本信息砾莱,映射的內(nèi)存大小瑞筐,加密crc的key等。
  • 7.m_crypter 默認是一個AESCrypt 對稱加密器
  • 8.m_lock ThreadLock線程鎖
  • 9.m_fileLock 一個以m_metaFile的fd 文件鎖
  • 10.m_sharedProcessLock 類型是InterProcessLock腊瑟,這是一種文件共享鎖
  • 11.m_exclusiveProcessLock 類型是InterProcessLock聚假,這是一種排他鎖
  • 12.m_isInterProcess 判斷是否打開了多進程模式的標志位,一旦關(guān)閉了闰非,所有進程鎖都會失效膘格。

Ashmem匿名共享內(nèi)存

Anonymous Shared Memory-Ashmem

簡單理解:

共享內(nèi)存是Linux自帶的一種IPC機制,Android直接使用了該模型财松,不過做出了自己的改進瘪贱,進而形成了Android的匿名共享內(nèi)存(Anonymous Shared Memory-Ashmem)

應用:

APP進程同SurfaceFlinger共用一塊內(nèi)存,如此辆毡,就不需要進行數(shù)據(jù)拷貝菜秦,APP端繪制完畢,通知SurfaceFlinger端合成胚迫,再輸出到硬件進行顯示即可

更多文章

http://www.reibang.com/p/6a8513fdb792

http://www.reibang.com/p/d9bc9c668ba6

多進程MMKV實例化

多進程通信的過程

        服務(wù)端創(chuàng)建MMKV實例
        m_ashmemMMKV = MMKV.mmkvWithAshmemID(BenchMarkBaseService.this, id, AshmemMMKV_Size,MMKV.MULTI_PROCESS_MODE, CryptKey);
        
        Aidl傳遞實體
        ParcelableMMKV(m_ashmemMMKV);
        
        Aidl傳遞實體 ParcelableMMKV字段
        mmapID = mmkv.mmapID();
        ashmemFD = mmkv.ashmemFD();
        ashmemMetaFD = mmkv.ashmemMetaFD();
        cryptKey = mmkv.cryptKey();
        
        客戶端獲取傳遞實體ParcelableMMKV
        AshmemMMKV ashmemMMKV = IAshmemMMKV.Stub.asInterface(service);
        ParcelableMMKV parcelableMMKV = ashmemMMKV.GetAshmemMMKV();
        
        客戶端獲取真正的操作數(shù)據(jù)的MMKV實例
        parcelableMMKV.toMMKV()
        public MMKV toMMKV() {
            if (ashmemFD >= 0 && ashmemMetaFD >= 0) {
                return MMKV.mmkvWithAshmemFD(mmapID, ashmemFD, ashmemMetaFD, cryptKey);
            }
            return null;
        }
        
        看一下mmkvWithAshmemFD
        MMKV.mmkvWithAshmemFD(mmapID, ashmemFD, ashmemMetaFD, cryptKey);

mmkvWithAshmemFD

MMKV *MMKV::mmkvWithAshmemFD(const string &mmapID, int fd, int metaFD, string *cryptKey) {

    if (fd < 0) {
        return nullptr;
    }
    SCOPED_LOCK(g_instanceLock);

    auto itr = g_instanceDic->find(mmapID);
    if (itr != g_instanceDic->end()) {
        MMKV *kv = itr->second;
#    ifndef MMKV_DISABLE_CRYPT
        kv->checkReSetCryptKey(fd, metaFD, cryptKey);
#    endif
        return kv;
    }
    auto kv = new MMKV(mmapID, fd, metaFD, cryptKey);
    (*g_instanceDic)[mmapID] = kv;
    return kv;
}


MMKV::MMKV(const string &mmapID, int ashmemFD, int ashmemMetaFD, string *cryptKey)
    : m_mmapID(mmapID)
    , m_path(mappedKVPathWithID(m_mmapID, MMKV_ASHMEM, nullptr))
    , m_crcPath(crcPathWithID(m_mmapID, MMKV_ASHMEM, nullptr))
    , m_dic(nullptr)
    , m_dicCrypt(nullptr)
    , m_file(new MemoryFile(ashmemFD))
    , m_metaFile(new MemoryFile(ashmemMetaFD))
    , m_metaInfo(new MMKVMetaInfo())
    , m_crypter(nullptr)
    , m_lock(new ThreadLock())
    , m_fileLock(new FileLock(m_metaFile->getFd(), true))
    , m_sharedProcessLock(new InterProcessLock(m_fileLock, SharedLockType))
    , m_exclusiveProcessLock(new InterProcessLock(m_fileLock, ExclusiveLockType))
    , m_isInterProcess(true) {

encode 寫入數(shù)據(jù)

encodeString

MMKV_JNI jboolean encodeString(JNIEnv *env, jobject, jlong handle, jstring oKey, jstring oValue) {
    MMKV *kv = reinterpret_cast<MMKV *>(handle);
    if (kv && oKey) {
        string key = jstring2string(env, oKey);
        if (oValue) {
            string value = jstring2string(env, oValue);
            return (jboolean) kv->set(value, key);
        } else {
            kv->removeValueForKey(key);
            return (jboolean) true;
        }
    }
    return (jboolean) false;
}
bool MMKV::set(const string &value, MMKVKey_t key) {
    if (isKeyEmpty(key)) {
        return false;
    }
    auto data = MiniPBCoder::encodeDataWithObject(value);
    return setDataForKey(std::move(data), key);
}

  • 1.encodeDataWithObject 編碼壓縮內(nèi)容
  • 2.setDataForKey 保存數(shù)據(jù)

setDataForKey

保存數(shù)據(jù)到映射的文件

bool MMKV::setDataForKey(MMBuffer &&data, MMKVKey_t key) {
    if (data.length() == 0 || isKeyEmpty(key)) {
        return false;
    }
    SCOPED_LOCK(m_lock);
    SCOPED_LOCK(m_exclusiveProcessLock);
    checkLoadData();

    auto ret = appendDataWithKey(data, key);
    if (ret) {
        m_dic[key] = std::move(data);
        m_hasFullWriteback = false;
    }
    return ret;
}

設(shè)置了互斥鎖喷户,和線程鎖。整個步驟分為兩步驟:

  • 1.checkLoadData 保存數(shù)據(jù)之前访锻,校驗已經(jīng)存儲的數(shù)據(jù)
  • 2.appendDataWithKey 進行數(shù)據(jù)的保存

appendDataWithKey

bool MMKV::appendDataWithKey(const MMBuffer &data, MMKVKey_t key) {

    size_t keyLength = key.length();
    // size needed to encode the key
    size_t size = keyLength + pbRawVarint32Size((int32_t) keyLength);
    // size needed to encode the value
    size += data.length() + pbRawVarint32Size((int32_t) data.length());

    SCOPED_LOCK(m_exclusiveProcessLock);

    bool hasEnoughSize = ensureMemorySize(size);
    if (!hasEnoughSize || !isFileValid()) {
        return false;
    }

    m_output->writeString(key);

    m_output->writeData(data); // note: write size of data

    auto ptr = (uint8_t *) m_file->getMemory() + Fixed32Size + m_actualSize;
    if (m_crypter) {
        m_crypter->encrypt(ptr, ptr, size);
    }
    m_actualSize += size;
    updateCRCDigest(ptr, size);

    return true;
}

判斷是否有足夠的空間褪尝,沒有則調(diào)用ensureMemorySize進行擴容闹获,實在無法從內(nèi)存中映射出來,那說明系統(tǒng)沒空間了就返回異常河哑。

正常情況下避诽,是往全局緩沖區(qū)CodedOutputData 先后在文件內(nèi)存的末尾寫入key和value的數(shù)據(jù)。并對這部分的數(shù)據(jù)進行一次加密璃谨,最后更新這個存儲區(qū)域的crc校驗碼沙庐。

這里實際上是調(diào)用了CodedOutputData的writeString把數(shù)據(jù)保存到映射的內(nèi)存中。

void CodedOutputData::writeString(const string &value) {
    size_t numberOfBytes = value.size();
    this->writeRawVarint32((int32_t) numberOfBytes);
    if (m_position + numberOfBytes > m_size) {
        auto msg = "m_position: " + to_string(m_position) + ", numberOfBytes: " + to_string(numberOfBytes) +
                   ", m_size: " + to_string(m_size);
        throw out_of_range(msg);
    }
    memcpy(m_ptr + m_position, ((uint8_t *) value.data()), numberOfBytes);
    m_position += numberOfBytes;
}

decode MMKV讀取數(shù)據(jù)

MMKV讀取數(shù)據(jù)

MMKV_JNI jstring decodeString(JNIEnv *env, jobject obj, jlong handle, jstring oKey, jstring oDefaultValue) {
    MMKV *kv = reinterpret_cast<MMKV *>(handle);
    if (kv && oKey) {
        string key = jstring2string(env, oKey);
        string value;
        bool hasValue = kv->getString(key, value);
        if (hasValue) {
            return string2jstring(env, value);
        }
    }
    return oDefaultValue;
}
bool MMKV::getString(MMKVKey_t key, string &result) {
    if (isKeyEmpty(key)) {
        return false;
    }
    SCOPED_LOCK(m_lock);
    auto &data = getDataForKey(key);
    if (data.length() > 0) {
        try {
            result = MiniPBCoder::decodeString(data);
            return true;
        } catch (std::exception &exception) {
            MMKVError("%s", exception.what());
        }
    }
    return false;
}

大致可以分分為兩步:

  • 1.getDataForKey 通過key找緩存的數(shù)據(jù)
  • 2.decodeString 對獲取到的數(shù)據(jù)進行解碼

getDataForKey

const MMBuffer &MMKV::getDataForKey(MMKVKey_t key) {
    checkLoadData();
    auto itr = m_dic.find(key);
    if (itr != m_dic.end()) {
        return itr->second;
    }
    static MMBuffer nan;
    return nan;
}

由于是一個多進程的組件佳吞,因此每一次進行讀寫之前都需要進行一次checkLoadData的校驗拱雏。而這個方法從上文可知,通過crc校驗碼底扳,寫回計數(shù)铸抑,文件長度來判斷文件是否發(fā)生了變更,是否追加刪除數(shù)據(jù)衷模,從而是否需要重新充內(nèi)存文件中獲取數(shù)據(jù)緩存到m_dic鹊汛。

也因此,在getDataForKey方法中阱冶,可以直接從m_dic中通過key找value刁憋。

decodeString

string MiniPBCoder::decodeString(const MMBuffer &oData) {
    MiniPBCoder oCoder(&oData);
    return oCoder.decodeOneString();
}
string MiniPBCoder::decodeOneString() {
    return m_inputData->readString();
}
string CodedInputData::readString() {
    int32_t size = readRawVarint32();
    if (size < 0) {
        throw length_error("InvalidProtocolBuffer negativeSize");
    }

    auto s_size = static_cast<size_t>(size);
    if (s_size <= m_size - m_position) {
        string result((char *) (m_ptr + m_position), s_size);
        m_position += s_size;
        return result;
    } else {
        throw out_of_range("InvalidProtocolBuffer truncatedMessage");
    }
}

能看到實際上很簡單就是從m_dic找到對應的MMBuffer數(shù)據(jù),此時的可以通過CodedInputData對MMBuffer對應的內(nèi)存塊(已經(jīng)知道內(nèi)存起始地址木蹬,長度)進行解析數(shù)據(jù)至耻。

總結(jié)

img

MMKV讀寫是直接讀寫到mmap文件映射的內(nèi)存上,繞開了普通讀寫io需要進入內(nèi)核届囚,寫到磁盤的過程有梆。光是這種級別優(yōu)化,都可以拉開三個數(shù)量級的性能差距意系。但是也誕生了一個很大的問題泥耀,一個進程在32位的機子中有4g的虛擬內(nèi)存限制,而我們把文件映射到虛擬內(nèi)存中蛔添,如果文件過大虛擬內(nèi)存就會出現(xiàn)大量的消耗最后出現(xiàn)異常痰催,對于不熟悉Linux的朋友就無法理解這種現(xiàn)象。

有幾個關(guān)于MMKV使用的注意事項:

  • 1.保證每一個文件存儲的數(shù)據(jù)都比較小迎瞧,也就說需要把數(shù)據(jù)根據(jù)業(yè)務(wù)線存儲分散夸溶。這要就不會把虛擬內(nèi)存消耗過快。
  • 2.還需要在適當?shù)臅r候釋放一部分內(nèi)存數(shù)據(jù)凶硅,比如在App中監(jiān)聽onTrimMemory方法缝裁,在Java內(nèi)存吃緊的情況下進行MMKV的trim操作(不準確,我們暫時以此為信號足绅,最好自己監(jiān)聽進程中內(nèi)存使用情況)捷绑。
  • 2.在不需要使用的時候韩脑,最好把MMKV給close掉。甚至調(diào)用exit方法粹污。

參考文章:http://www.reibang.com/p/c12290a9a3f7

官方Demo:https://github.com/Tencent/MMKV/tree/master/Android/MMKV

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末段多,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子壮吩,更是在濱河造成了極大的恐慌进苍,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鸭叙,死亡現(xiàn)場離奇詭異觉啊,居然都是意外死亡,警方通過查閱死者的電腦和手機递雀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進店門柄延,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人缀程,你說我怎么就攤上這事∈锌。” “怎么了杨凑?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長摆昧。 經(jīng)常有香客問我撩满,道長,這世上最難降的妖魔是什么绅你? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任伺帘,我火速辦了婚禮,結(jié)果婚禮上忌锯,老公的妹妹穿的比我還像新娘伪嫁。我一直安慰自己,他們只是感情好偶垮,可當我...
    茶點故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布张咳。 她就那樣靜靜地躺著,像睡著了一般似舵。 火紅的嫁衣襯著肌膚如雪脚猾。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天砚哗,我揣著相機與錄音龙助,去河邊找鬼。 笑死蛛芥,一個胖子當著我的面吹牛提鸟,可吹牛的內(nèi)容都是我干的脆淹。 我是一名探鬼主播,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼沽一,長吁一口氣:“原來是場噩夢啊……” “哼盖溺!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起铣缠,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤烘嘱,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蝗蛙,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蝇庭,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年捡硅,在試婚紗的時候發(fā)現(xiàn)自己被綠了哮内。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,488評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡壮韭,死狀恐怖北发,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情喷屋,我是刑警寧澤琳拨,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站屯曹,受9級特大地震影響狱庇,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜恶耽,卻給世界環(huán)境...
    茶點故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一密任、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧偷俭,春花似錦浪讳、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至形葬,卻和暖如春合呐,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背笙以。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工淌实, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓拆祈,卻偏偏與公主長得像恨闪,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子放坏,可洞房花燭夜當晚...
    茶點故事閱讀 45,500評論 2 359

推薦閱讀更多精彩內(nèi)容