首页 > 其他分享 >Android性能优化:微信自用高性能持久化框架——MMKV组件原理

Android性能优化:微信自用高性能持久化框架——MMKV组件原理

时间:2023-06-28 12:33:13浏览次数:49  
标签:微信 MMKV mmkv 空间 内存 mmap 进程 Android


MMKV

MMKV——基于 mmap 的高性能通用 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。
github

MMKV 是基于 mmap 内存映射的移动端通用 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。
从 2015 年中至今,在 iOS 微信上使用已有近 3 年,其性能和稳定性经过了时间的验证。
近期已移植到 Android 平台。在腾讯内部开源半年之后,得到公司内部团队的广泛应用和一致好评。

通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,
由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。

XML、JSON 更注重数据结构化,关注人类可读性和语义表达能力。
ProtoBuf 更注重数据序列化,关注效率、空间、速度,人类可读性差,语义表达能力不足(为保证极致的效率,会舍弃一部分元信息)

特点

  • 高性能 实时写入
  • 稳定 防crash
  • 多进程访问
    通过与 Android 开发同学的沟通,了解到系统自带的 SharedPreferences 对多进程的支持不好。
    现有基于 ContentProvider 封装的实现,虽然多进程是支持了,但是性能低下,经常导致 ANR。
    考虑到 mmap 共享内存本质上的多进程共享的,我们在这个基础上,深入挖掘了 Android 系统的能力,提供了可能是业界最高效的多进程数据共享组件。
  • 匿名内存
    在多进程共享的基础上,考虑到某些敏感数据(例如密码)需要进程间共享,但是不方便落地存储到文件上,直接用 mmap 不合适。
    我们了解到 Android 系统提供了 Ashmem 匿名共享内存的能力,发现它在进程退出后就会消失,不会落地到文件上,非常适合这个场景。
    我们很愉快地提供了 Ashmem MMKV 的功能。
  • 数据加密
    不像 iOS 提供了硬件层级的加密机制,在 Android 环境里,数据加密是非常必须的。
    MMKV 使用了 AES CFB-128 算法来加密/解密。我们选择 CFB 而不是常见的 CBC 算法,
    主要是因为 MMKV 使用 append-only 实现插入/更新操作,流式加密算法更加合适。
  • 数据有效性

MMKV 原理

  • 内存准备
    通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。
  • 数据组织
    数据序列化方面我们选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现。
  • 写入优化
    考虑到主要使用场景是频繁地进行写入更新,我们需要有增量更新的能力。我们考虑将增量 kv 对象序列化后,append 到内存末尾
    这样同一个 key 会有新旧若干份数据,最新的数据在最后;那么只需在程序启动第一次打开 mmkv 时,不断用后读入的 value 替换之前的值,就可以保证数据是最新有效的。
  • 空间增长
    使用 append 实现增量更新带来了一个新的问题,就是不断 append 的话,文件大小会增长得不可控。我们需要在性能和空间上做个折中。
    以内存 pagesize 为单位申请空间,在空间用尽之前都是 append 模式;当 append 到文件末尾时,进行文件重整、key 排重,尝试序列化保存排重结果;
    排重后空间还是不够用的话,将文件扩大一倍,直到空间足够。
  • 数据有效性
    考虑到文件系统、操作系统都有一定的不稳定性,我们另外增加了 crc 校验,对无效数据进行甄别。

更详细的设计原理参考 MMKV 原理

快速上手

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

MMKV的使用非常简单,
所有变更立马生效,无需调用 sync、apply。
在 App 启动时初始化 MMKV,设定 MMKV 的根目录
(默认/data/data/xxx.xxx/files/mmkv/)
(sp存储在/data/data/xxx.xxx/shared_prefs/)

支持从SP迁移数据importFromSharedPreferences

MMKV 还额外实现了一遍 SharedPreferences、SharedPreferences.Editor 这两个 interface

// 可以跟SP用法一样
SharedPreferences.Editor editor = mmkv.edit();
// 无需调用 commit()
//editor.commit();

MMKV 的使用非常简单,所有变更立马生效,无需调用 sync、apply。 在 App 启动时初始化 MMKV,设定 MMKV 的根目录(files/mmkv/),例如在 MainActivity 里:

 

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    String rootDir = MMKV.initialize(this);
    System.out.println("mmkv root: " + rootDir);
    //……
}

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");

使用完毕的几个方法

public native void clearAll();

    // MMKV's size won't reduce after deleting key-values
    // call this method after lots of deleting f you care about disk usage
    // note that `clearAll` has the similar effect of `trim`
    public native void trim();

    // call this method if the instance is no longer needed in the near future
    // any subsequent call to the instance is undefined behavior
    public native void close();

    // call on memory warning
    // any subsequent call to the instance will load all key-values from file again
    public native void clearMemoryCache();

    // you don't need to call this, really, I mean it
    // unless you care about out of battery
    public void sync() {
        sync(true);
    }

性能对比

我们将 MMKV 和 SharedPreferences、SQLite 进行对比, 重复读写操作 1k 次。相关测试代码在 Android/MMKV/mmkvdemo/。结果如下图表。

单进程性能

Android性能优化:微信自用高性能持久化框架——MMKV组件原理_android

可见,MMKV 在写入性能上远远超越 SharedPreferences & SQLite,在读取性能上也有相近或超越的表现。

多进程性能

Android性能优化:微信自用高性能持久化框架——MMKV组件原理_android_02

可见,MMKV 无论是在写入性能还是在读取性能,都远远超越 MultiProcessSharedPreferences & SQLite & SQLite,
MMKV 在 Android 多进程 key-value 存储组件上是不二之选。

补充适用建议

如果使用请务必做code19版本的适配,这个在github官网有说明

依赖下面这个库,然后对19区分处理
implementation ‘com.getkeepsafe.relinker:relinker:1.3.1’

 

if (android.os.Build.VERSION.SDK_INT == 19) {
    MMKV.initialize(relativePath, new MMKV.LibLoader() {
        @Override
        public void loadLibrary(String libName) {
            ReLinker.loadLibrary(context, libName);
        }
    });
} else {
    MMKV.initialize(context);
}

限制

可看到,一个键会存入多分实例,最后存入的就是最新的。
MMKV 在大部分情况下都性能强劲,key/value 的数量和长度都没有限制。
然而 MMKV 在内存里缓存了所有的 key-value,在总大小比较大的情况下(例如 100M+),App 可能会爆内存,触发重整回写时,写入速度也会变慢。
支持大文件的 MMKV 正在开发中,有望在下一个大版本发布。

问题

数据变化监听 怎么获取?

// content change notification of other process
// trigger by getXXX() or setXXX() or checkContentChangedByOuterProcess()

多进程 issue //CallStaticVoidMethod 错误写成 CallStaticIntMethod,方法匹配crash

registerOnSharedPreferenceChangeListener not support
//官方推荐使用event方式通知更新
Data-change-listener is not supported by design.
We suggest using something like event-bus to notify any interesting clients.
Doing this inside a storage framework smells really bad.

defaultMMKV 是单进程SINGLE_PROCESS_MODE
使用MULTI_PROCESS_MODE创建多进程

带来的APK尺寸增加问题

libc++_shared.so 252.5k

libmmkv.so 43.5k

implementation 'com.tencent:mmkv:1.0.23'

// implementation 'com.tencent:mmkv-static:1.0.23' (无libc++_shared.so)

只打包需要的平台对应.so

ndk {
            abiFilters "armeabi-v7a", 'x86'
        }

.so加载问题

implementation 'com.getkeepsafe.relinker:relinker:1.3.1'

log太多

初始化可以设置log打印层级 initialize(rootDir, MMKVLogLevel.LevelInfo);
设置log转发,控制log输出格式、文件 MMKVHandler wantLogRedirecting=true

多进程

锁 lock unlock tryLock

注意如果一个进程lock住,另一个进程mmkvWithID获取MMKV时就阻塞住,直到持有进程释放。

// get the lock immediately
        MMKV mmkv2 = MMKV.mmkvWithID(LOCK_PHASE_2, MMKV.MULTI_PROCESS_MODE);
        mmkv2.lock();
        Log.d("locked in child", LOCK_PHASE_2);

        Runnable waiter = new Runnable() {
            @Override
            public void run() {
                //阻塞住 直到其他进程释放
                MMKV mmkv1 = MMKV.mmkvWithID(LOCK_PHASE_1, MMKV.MULTI_PROCESS_MODE);
                mmkv1.lock();
                Log.d("locked in child", LOCK_PHASE_1);
            }
        };

注意:如果其他进程有进行修改,不会立即触发onContentChangedByOuterProcess,
checkLoadData如果变化,会clearMemoryState,重新loadFromFile。//数据量大时不要太频繁
读取decodeXXX会阻塞住,先回调onContentChangedByOuterProcess,再返回值,保证值是最新的。

mmkvWithAshmemID 匿名共享内存

可以进行进程间通信,可设置pageSize
// a memory only MMKV, cleared on program exit
// size cannot change afterward (because ashmem won't allow it)

测试

write速度 mmkv > cryptKV >> sp
read速度 sp > cryptKV > mmkv

Binder MMAP(一次拷贝)

Linux的内存分用户空间跟内核空间,同时页表有也分两类,用户空间页表跟内核空间页表,每个进程有一个用户空间页表,但是系统只有一个内核空间页表。
而Binder mmap的关键是:更新用户空间对应的页表的同时也同步映射内核页表,让两个页表都指向同一块地址,
这样一来,数据只需要从A进程的用户空间,直接拷贝到B所对应的内核空间,而B多对应的内核空间在B进程的用户空间也有相应的映射,这样就无需从内核拷贝到用户空间了。

copy_from_user() //将数据从用户空间拷贝到内核空间
copy_to_user() //将数据从内核空间拷贝到用户空间

Liunx进程隔离

Android性能优化:微信自用高性能持久化框架——MMKV组件原理_android_03

传统IPC

Android性能优化:微信自用高性能持久化框架——MMKV组件原理_android_04

Binder通信

Android性能优化:微信自用高性能持久化框架——MMKV组件原理_android_05

普通文件mmap原理

普通文件的访问方式有两种:

第一种是通过read/write系统调访问,先在用户空间分配一段buffer,然后,进入内核,将内容从磁盘读取到内核缓冲,最后,拷贝到用户进程空间,至少牵扯到两次数据拷贝;
同时,多个进程同时访问一个文件,每个进程都有一个副本,存在资源浪费的问题。

另一种是通过mmap来访问文件,mmap()将文件直接映射到用户空间,文件在mmap的时候,内存并未真正分配,
只有在第一次读取/写入的时候才会触发,这个时候,会引发缺页中断,在处理缺页中断的时候,完成内存也分配,同时也完成文件数据的拷贝。
并且,修改用户空间对应的页表,完成到物理内存到用户空间的映射,这种方式只存在一次数据拷贝,效率更高。
同时多进程间通过mmap共享文件数据的时候,仅需要一块物理内存就够了。

Android中使用mmap,可以通过RandomAccessFile与MappedByteBuffer来配合。
通过randomAccessFile.getChannel().map获取到MappedByteBuffer。然后调用ByteBuffer的put方法添加数据。

 

RandomAccessFile randomAccessFile = new RandomAccessFile("path","rw");
        MappedByteBuffer mappedByteBuffer= randomAccessFile.getChannel().map(FileChannel.MapMode.READ_WRITE,0, randomAccessFile.length());

        mappedByteBuffer.putChar('c');
        mappedByteBuffer.getChar();

共享内存中mmap的使用

共享内存是在普通文件mmap的基础上实现的,其实就是基于tmpfs文件系统的普通mmap。

有任何问题欢迎指点。

标签:微信,MMKV,mmkv,空间,内存,mmap,进程,Android
From: https://blog.51cto.com/u_16163453/6570430

相关文章

  • 精选Android中高级高频面试题:四大组件及Fragment原理
    因为实际开发与参考答案会有所不同,再者怕误导大家,所以这些面试题答案还是自己去理解!面试官会针对简历中提到的知识点由浅入深提问,所以不要背答案,多理解。Activity1、说下Activity生命周期?参考解答:在正常情况下,Activity的常用生命周期就只有如下7个onCreate():表示Activity正在被创......
  • 精选Android中高级面试题:性能优化,JNI,设计模式
    性能优化1、图片的三级缓存中,图片加载到内存中,如果内存快爆了,会发生什么?怎么处理?参考回答:首先我们要清楚图片的三级缓存是如何的:如果内存足够时不回收。内存不够时就回收软引用对象2、内存中如果加载一张500*500的png高清图片。应该是占用多少的内存?不考虑屏幕比的话:占用内存......
  • Android知识笔记:记录 2 个 “容易误解” 的Android 知识点
    今天分享两个之前我们可能都搞错的Android知识点,我们还是要追求极致,把不懂的问题搞懂的~1.事件到底是先到DecorView还是先到Window的?有天早上看到事件分发的一个讨论:那么事件到底是先到DecorView还是先到Window(Activity,Dialog)的呢,引发出两个问题:1.touch相关事件在DecorView,Phon......
  • 记一次Android奇葩面试经历:因为没去过BAT,我被面试官“轰”出门外
    最近面试了几家大规模的公司,也遇到了各种各种的问题,技术方面的,管理方面的都有涉及。让我印象最深刻的是某上市公司,自称是阿里的控股子公司,创始人团队来自于阿里,感觉很高大上的样子。进门之后就是填表,然后就是技术负责人面试,问了一些项目中的问题。有的没的扯一大堆,对技术不是很看中......
  • Android 中高级面试原理:热修复与插件化基础—Java与Android虚拟机
    一、Java虚拟机(JVM)1、JVM整体结构使用javac将java文件编译成class文件。类加载器(ClassLoader)将class字节码加载进JVM对应的内存中。JVM将内存分配给方法区、堆区、栈区、本地方式栈4个部分,这4个部分分别存储字节码不同的部分。垃圾回收器(gc)会管理整个内存空间中的垃圾。2、Java代码......
  • BAT 大厂Android研发岗必刷真题:Android异常与性能优化相关面试问题
    今天来讲一讲在面试中碰到的Android异常与性能优化相关问题:1、anr异常面试问题讲解a)什么是anr?应用程序无响应对话框b)造成anr的原因?**主线程中做了耗时操作c)android中那些操作是在主线程呢?activity的所有生命周期回调都是执行在主线程的Service默认是执行在主线程的BroadcastR......
  • Android LayoutManager高端玩家,实现花式表格!
    如果你对RecyclerView原理还不是特别了解,非常建议你读一下。本文的项目也是学习自定义LayoutManager绝佳资料,大家有需要的可以好好拜读。前言表格是自打我进公司以后就使用的控件,起初使用的是ScrollablePanel,从一开始的被花式吊打,到后期的熟练使用。大佬写的控件确实给我的工作带来......
  • Android ‘Handler()‘ is deprecated
    privateHandlerhandler=newHandler();Handler()此构造函数在Android11/R之后已弃用。在Handler构造期间隐式选择Looper会导致操作无声地丢失(如果Handler不期待新任务并退出)、崩溃(如果有时在没有Looper活动的线程上创建处理程序)或竞争条件,处理程序关联的线程不......
  • Android线程管理之ExecutorService线程池
    为什么要引入线程池?   1.)newThread()的缺点每次newThread()耗费性能调用newThread()创建的线程缺乏管理,被称为野线程,而且可以无限制创建,之间相互竞争,会导致过多占用系统资源导致系统瘫痪。不利于扩展,比如如定时执行、定期执行、线程中断  2.)采用线程池的优点重用存在的......
  • 前端Vue自定义微信支付弹框dialog alert popup
    前端Vue自定义微信支付弹框dialogalertpopup, 下载完整代码请访问uni-app插件市场地址:https://ext.dcloud.net.cn/plugin?id=13245效果图如下:实现代码如下:cc-payDialog使用方法<!--:money:支付金额 show:是否显示@cancel:取消 @success:确认支付--><cc-payDia......