Skip to content

android_advance_cn

guoling edited this page Dec 6, 2024 · 7 revisions

MMKV for Android

MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。从 2015 年中至今在微信上使用,其性能和稳定性经过了时间的验证。近期也已移植到 Android / macOS / Windows / POSIX 平台,一并开源。

Android 进阶

MMKV 有一些高级设置,可以使得更符合你的需求。

日志

  • MMKV 默认将日志打印到 logcat,不便于对线上问题进行定位和解决。你可以在 App 启动时接收转发 MMKV 的日志。实现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;
        }
    }

    至于使用哪个客户端日志组件,我们推荐使用 xlog,同样也是开源自微信团队。

  • 如果你不希望 MMKV 打印日志,你可以关掉它(虽然我们强烈不建议你这么做)。
    注意:除非有非常强烈的证据表明MMKV的日志拖慢了App的速度,你不应该关掉日志。没有日志,日后万一用户有问题,将无法跟进。

    MMKV.setLogLevel(MMKVLogLevel.LevelNone);

加密

  • MMKV 默认明文存储所有 key-value,依赖 Android 系统的沙盒机制保证文件加密。如果你担心信息泄露,你可以选择加密 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);

自定义根目录

  • MMKV 默认把文件存放在$(FilesDir)/mmkv/目录。你可以在 App 启动时自定义根目录:

    String dir = getFilesDir().getAbsolutePath() + "/mmkv_2";
    String rootDir = MMKV.initialize(this, dir);
    Log.i("MMKV", "mmkv root: " + rootDir);
  • MMKV 甚至支持自定义某个文件的目录:

    String relativePath = getFilesDir().getAbsolutePath() + "/mmkv_3";
    MMKV kv = MMKV.mmkvWithID("testCustomDir", relativePath);

    注意:官方推荐将 MMKV 文件存储在你 App 的私有路径内部不要 存储在 external storage(也就是 SD card)。如果你一定要这样做,你应该遵循 Android 的 scoped storage 指引。

自定义 library loader

  • 一些 Android 设备(API level 19)在安装/更新 APK 时可能出错, 导致 libmmkv.so 找不到。然后就会遇到 java.lang.UnsatisfiedLinkError 之类的 crash。有个开源库 ReLinker 专门解决这个问题,你可以用它来加载 MMKV:

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

Native Buffer

  • 当从 MMKV 取一个 String or byte[]的时候,会有一次从 native 到 JVM 的内存拷贝。如果这个值立即传递到另一个 native 库(JNI),又会有一次从 JVM 到 native 的内存拷贝。当这个值比较大的时候,整个过程会非常浪费。Native Buffer 就是为了解决这个问题。
    Native Buffer 是由 native 创建的内存缓冲区,在 Java 里封装成 NativeBuffer 类型,可以透明传递到另一个 native 库进行访问处理。整个过程避免了先拷内存到 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);
    }

数据恢复

  • 在 crc 校验失败,或者文件长度不对的时候,MMKV 默认会丢弃所有数据。你可以让 MMKV 恢复数据。要注意的是修复率无法保证,而且可能修复出奇怪的 key-value。同样地也是实现MMKVHandler接口,添加以下代码:

    @Override
    public MMKVRecoverStrategic onMMKVCRCCheckFail(String mmapID) {
        return MMKVRecoverStrategic.OnErrorRecover;
    }
    
    @Override
    public MMKVRecoverStrategic onMMKVFileLengthError(String mmapID) {
        return MMKVRecoverStrategic.OnErrorRecover;
    }

备份 & 恢复

  • MMKV 提供了备份和恢复接口,可用于备份数据到其他目录,并稍后恢复原有数据。

    String backupRootDir = getFilesDir().getAbsolutePath() + "/mmkv_backup_3";
    // backup one instance
    boolean ret = MMKV.backupOneToDirectory(mmapID, backupRootDir, null);
    // backup all instances
    long count = MMKV.backupAllToDirectory(backupRootDir);
    
    // restore one instance
    ret = MMKV.restoreOneMMKVFromDirectory(mmapID, backupRootDir, otherDir);
    // restore all instances
    count = MMKV.restoreAllFromDirectory(backupRootDir);

自动过期

  • v1.3.0 起你可以升级 MMKV 到 key 自动过期特性。注意这是格式不向下兼容的升级操作。一旦升级到 key 自动过期,旧版 MMKV (<= v1.2.16) 将无法正常读写该文件。

  • 全局过期. 最简单的用法是给整个文件设定统一的过期间隔。

    // expire in a day
    mmkv.enableAutoKeyExpire(MMKV.ExpireInDay); // MMKV.ExpireInDay = 24 * 60 * 60

    或者,你可以选择只升级 MMKV,但不设置全局过期间隔。这种情况下,默认每个 key 不过期。

    // enable auto key expiration without global duration
    mmkv.enableAutoKeyExpire(MMKV.ExpireNever); // MMKV.ExpireNever = 0
  • 单独过期. 你可以给每个 key 设置单独的过期间隔,区分于文件的全局过期间隔。注意,你仍然需要先升级 MMKV 为 key 自动过期

    // enable auto key expiration with an hour duration
    mmkv.enableAutoKeyExpire(MMKV.ExpireInHour); // MMKV.ExpireInHour = 60 * 60
    
    // set a key with the file's global expiration duration, aka MMKV.ExpireInHour
    mmkv.encode("key_1", "some value");
    
    // set a special key that expires in two hours
    mmkv.encode("key_2", "some value", 2 * 60 * 60);
    
    // set a special key that never expires
    mmkv.encode("key_3", "some value", MMKV.ExpireNever);

    或者,你可以选择只升级 MMKV,但不设置全局过期间隔。这种情况下,默认每个 key 不过期。

    // enable auto key expiration without global duration
    mmkv.enableAutoKeyExpire(MMKV.ExpireNever); // MMKV.ExpireNever = 0
    
    // set a key that never expires
    mmkv.encode("key_1", "some value");
    
    // set a special key that expires in an hour
    mmkv.encode("key_2", "some value", MMKV.ExpireInHour);
  • 过期间隔的单位是秒。MMKV 预定义了一些常用间隔,方便大家使用。你也可以使用任意自定义间隔,例如一周是 7 * 24 * 60 * 60

    ExpireNever = 0;
    ExpireInMinute = 60;
    ExpireInHour = 60 * 60;
    ExpireInDay = 24 * 60 * 60;
    ExpireInMonth = 30 * 24 * 60 * 60;
    ExpireInYear = 365 * 30 * 24 * 60 * 60;

下一步

Clone this wiki locally