1.如何跨进程
2.为什么效率高
3.如何扩容
4.probuffer数据结构
5.binder机制
6.用户空间和内核空间
7.内存映射
目前项目中在轻量级存储上使用的是 SharedPreferences, 虽然 SP 兼容性极好, 但 SP 的低性能一直被诟病, 线上也出现了一些因为 SP 导致的 ANR
sp卡顿的原因
sp:也是文件,存放在内部目录,xml方式
比起 SP 的数据同步, mmap 显然是要优雅的多, 即使进程意外死亡, 也能够通过 Linux 内核的保护机制, 将进行了文件映射的内存数据刷入到文件中, 提升了数据写入的可靠性
通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。
数据的编码
MMKV 采用的是 ProtocolBuffer 编码方式,
什么时候增量更新?
什么时候全量更新?
标准 protobuf 不提供增量更新的能力,每次写入都必须全量写入。考虑到主要使用场景是频繁地进行写入更新,我们需要有增量更新的能力:将增量 kv 对象序列化后,直接 append 到内存末尾;这样同一个 key 会有新旧若干份数据,最新的数据在最后;那么只需在程序启动第一次打开 mmkv 时,不断用后读入的 value 替换之前的值,就可以保证数据是最新有效的。
空间增长: (还有这里!!!)
使用 append 实现增量更新带来了一个新的问题,就是不断 append 的话,文件大小会增长得不可控。例如同一个 key 不断更新的话,是可能耗尽几百 M 甚至上 G 空间,而事实上整个 kv 文件就这一个 key,不到 1k 空间就存得下。这明显是不可取的。我们需要在性能和空间上做个折中:以内存 pagesize 为单位申请空间,在空间用尽之前都是 append 模式;当 append 到文件末尾时,进行文件重整、key 排重,尝试序列化保存排重结果;排重后空间还是不够用的话,将文件扩大一倍,直到空间足够。
MMKV for Android 特有功能
我们不是简简单单地照搬 iOS 的实现,在迁移到 Android 的过程中,深入分析了 Android 平台现有 kv 组件的痛点,在原有功能基础上,开发了 Android 特有的功能。
- 多进程访问
通过与 Android 开发同学的沟通,了解到系统自带的 SharedPreferences 对多进程的支持不好。现有基于 ContentProvider 封装的实现,虽然多进程是支持了,但是性能低下,经常导致 ANR。考虑到 mmap 共享内存本质上的多进程共享的,我们在这个基础上,深入挖掘了 Android 系统的能力,提供了可能是业界最高效的多进程数据共享组件。具体实现原理我们中秋节后分享,心急的同学可以前往 GitHub 查看源码和 wiki 文档。 - 匿名内存
在多进程共享的基础上,考虑到某些敏感数据(例如密码)需要进程间共享,但是不方便落地存储到文件上,直接用 mmap 不合适。我们了解到 Android 系统提供了 Ashmem 匿名共享内存的能力,发现它在进程退出后就会消失,不会落地到文件上,非常适合这个场景。我们很愉快地提供了 Ashmem MMKV 的功能。 - 数据加密
不像 iOS 提供了硬件层级的加密机制,在 Android 环境里,数据加密是非常必须的。MMKV 使用了 AES CFB-128 算法来加密/解密。我们选择 CFB 而不是常见的 CBC 算法,主要是因为 MMKV 使用 append-only 实现插入/更新操作,流式加密算法更加合适。事实上这个功能也回馈到了 iOS 版,所以现在两个系统的 MMKV 都有加密功能。
如何扩容?数据的重整与扩容
用户空间和内核空间
1.mmap:内存映射:binder机制也是通过mmap实现的
2.通过c++
3.通过c来。操作文件,===绑定了一个数组,直接操作数组
基于 mmap 又是如何实现一次拷贝的?
mmap内存映射原理:
mmap是Linux中常用的系统调用API,用途广泛,Android中也有不少地方用到,比如匿名共享内存,Binder机制等。本文简单记录下Android中mmap调用流程及原理。mmap函数原型如下:
c++。文件写入流程
MmapedFile 的构造函数处理的事务如下
- 打开指定的文件
- 创建这个文件锁
- 修正文件大小, 最小为 4kb
- 前 4kb 用于统计数据总大小
- 通过 mmap 将文件映射到内存
- 多个进程 map 同一个对象,可以共享数据。
为什么快1000倍:
app ——内核空间——磁盘
app——磁盘
1.2好处
- 应用程序崩溃不会造成内核崩溃,拿windows举例来说,QQ崩溃掉不会造成程序死机。
- 每个应用程序或者进程都会有自己特定的地址、私有数据空间,程序之间一般不会相互影响 例如QQ崩溃不会造成微信的崩溃。空间的隔离极大地提高了系统运行的稳定性。
计算机蓝屏带来的启示
计算机蓝屏主要是因为计算机硬件驱动不兼容问题造成,硬件驱动代码运行在内核空间,与kernel运行在相同空间内,所以驱动程序发生问题容易造成系统的崩溃。将用户空间与内核空间隔离开,可减少系统崩溃的可能,提高系统的稳定性。毕竟现实情况中,应用程序崩溃的情况比蓝屏出现的概率要多的多得多。在linux中这种情况可以类比
普通文件mmap原理
普通文件的访问方式有两种:第一种是通过read/write系统调访问,先在用户空间分配一段buffer,然后,进入内核,将内容从磁盘读取到内核缓冲,最后,拷贝到用户进程空间,至少牵扯到两次数据拷贝;同时,多个进程同时访问一个文件,每个进程都有一个副本,存在资源浪费的问题。
另一种是通过mmap来访问文件,mmap()将文件直接映射到用户空间,文件在mmap的时候,内存并未真正分配,只有在第一次读取/写入的时候才会触发,这个时候,会引发缺页中断,在处理缺页中断的时候,完成内存也分配,同时也完成文件数据的拷贝。并且,修改用户空间对应的页表,完成到物理内存到用户空间的映射,这种方式只存在一次数据拷贝,效率更高。同时多进程间通过mmap共享文件数据的时候,仅需要一块物理内存就够了
面试官:怎么理解页框和页?
🤔️:页框是指一块实际的物理内存,页是指程序的一块内存数据单元。内存数据一定是存储在实际的物理内存上,即页必然对应于一个页框,页数据实际是存储在页框上的。
页框和页一样大,都是内核对内存的分块单位。一个页框可以映射给多个页,也就是说一块实际的物理存储空间可以映射给多个进程的多个虚拟内存空间,这也是 mmap 机制依赖的基础规则
linux使用MMU的机器都采用分页机制。虚拟地址空间以页为单位进行划分,而相应的物理地址空间也被划分,其使用的单位称为页帧,页帧和页必须保持相同,因为内存与外部存储器之间的传输是以页为单位进行传输的。
例如,MMU可以通过一个映射项将VA的一页0xb70010000xb7001fff映射到PA的一页0x20000x2fff,如果CPU执行单元要访问虚拟地址0xb7001008,则实际访问到的物理地址是0x2008。
虚拟内存的哪个页面映射到物理内存的哪个页帧是通过页表(Page Table)来描述的,页表保存在物理内存中,MMU会查找页表来确定一个VA应该映射到什么PA。
总结
通过上面的分析, 我们对 MMKV 有了一个整体上的把控, 其具体的表现如下所示
项目 | 评价 | 描述 |
正确性 | 优 | 支持多进程安全, 使用 mmap, 由操作系统保证数据回写的正确性 |
时间开销 | 优 | 使用 mmap 实现, 减少了用户空间数据到内核空间的拷贝 |
空间开销 | 中 | 使用 protocl buffer 存储数据, 同样的数据会比 xml 和 json 消耗空间小 使用的是数据追加到末尾的方式, 只有到达一定阈值之后才会触发键值合并, 不合并之前会导致同一个 key 存在多份 |
安全 | 中 | 使用 crc 校验, 甄别文件系统和操作系统不稳定导致的异常数据 |
开发成本 | 优 | 使用方式较为简单 |
兼容性 | 优 | 各个安卓版本都前后兼容 |
虽然 MMKV 一些场景下比 SP 稍慢(如: 首次实例化会进行数据的复写剔除重复数据, 比 SP 稍慢, 查询数据时存在 ProtocolBuffer 解码, 比 SP 稍慢), 但其逆天的数据写入速度、mmap Linux 内核保证数据的同步, 以及 ProtocolBuffer 编码带来的更小的本地存储空间占用等都是非常棒的闪光点