存储性能优化 MMKV源码解析
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了存储性能优化 MMKV源码解析相关的知识,希望对你有一定的参考价值。
参考技术A 好久没有更新常用的第三方库了。让我们来聊聊MMKV这个常用的第三方库。MMKV这个库是做什么的呢?他本质上的定位和sp有点相似,经常用于持久化小数据的键值对。其速度可以说是当前所有同类型中速度最快,性能最优的库。它的最早的诞生,主要是因为在微信ios端有一个重大的bug,一个特殊的文本可以导致微信的iOS端闪退,而且还出现了不止一次。为了统计这种闪退的字符出现频率以及过滤,但是由于出现的次数,发现原来的键值对存储组件NSUserDefaults根本达不到要求,会导致cell的滑动卡顿。
因此iOS端就开始创造一个高新性能的键值对存储组件。于此同时,android端SharedPreferences也有如下几个缺点:
因此Android也开始复用iOS的MMKV,而后Android有了多进程的写入数据的需求,Android组又在这个基础上进行改进。
这里是官方的性能的比较图:
能看到mmkv比起我们开发常用的组件要快上数百倍。
那么本文将会从源码角度围绕MMKV的性能为什么会如此高,以及SharePrefences为什么可能出现ANR的原因。
请注意下文是以MMKV 1.1.1版本源码为例子分析。如果遇到什么问题欢迎来到本文 https://www.jianshu.com/p/c12290a9a3f7 互相讨论。
老规矩,先来看看MMKV怎么使用。mmkv其实和SharePrefences一样,有增删查改四种操作。
MMKV作为一个键值对存储组件,也对了存储对象的序列化方式进行了优化。常用的方式比如有json,Twitter的Serial。而MMKV使用的是Google开源的序列化方案:Protocol Buffers。
Protocol Buffers这个方案比起json来说就高级不少:
使用方式可以阅读下面这篇文章: https://www.jianshu.com/p/e8712962f0e9
下面进行比较几个对象序列化之间的要素比较
而MMKV就是看重了Protocol Buffers的时间开销小,选择Protocol Buffers进行对象缓存的核心。
使用前请初始化:
当然mmkv除了能够写入这些基本类型,只要SharePrefences支持的,它也一定能够支持。
同上,每一个key读取的数据类型就是decodexxx对应的类型名字。使用起来十分简单。
能够删除单个key对应的value,也能删除多个key分别对应的value。containsKey判断mmkv的磁盘缓存中是否存在对应的key。
mmkv和SharePrefences一样,还能根据模块和业务划分对应的缓存文件:
这里创建了一个id为a的实例在磁盘中,进行数据的缓存。
当需要多进程缓存的时候:
MMKV可以使用Ashmem的匿名内存进行更加快速的大对象传输:
进程1:
最重要的一点,mmkv把SharePrefences的缓存迁移到mmkv中,之后的使用就和SharePrefences一致。
这里就是把SharedPreferences的myData数据迁移到mmkv中。当然如果我们需要保持SharePreferences的用法不变需要自己进行自定义一个SharePreferences。
mmkv的用法极其简单,接下来我们关注他的原理。
首先来看看MMKV的初始化。
能看到实际上initialize分为如下几个步骤:
能看到其实就是做这个判断。由于此时设置的是libc++的打包方式。此时BuildConfig.FLAVOR就是StaticCpp,就不会加载c++_shared。当然,如果我们已经使用了c++_shared库,则没有必要打包进去,使用defaultPublishConfig "SharedCppRelease"会尝试的查找动态链接库_shared。这样就能少2M的大小。
请注意一个前提的知识,jni的初始化,在调用了 System.loadLibrary之后,会通过dlopen把so加载到内存后,调用dlsym,调用jni中的JNI_OnLoad方法。
实际上这里面做的事情十分简单:
能从这些native方法中看到了所有MMKV的存储方法,设置支持共享内存ashemem的存储,支持直接获取native malloc申请的内存
接下来就是MMKV正式的初始化方法了。
这个方法实际上调用的是pthread_once方法。它一般是在多线程环境中,根据内核的调度策略,选择一个线程初始化一次的方法。
其实这里面的算法很简单:
defaultMMKV此时调用的是getDefaultMMKV这个native方法,默认是单进程模式。从这里的设计都能猜到getDefaultMMKV会从native层实例化一个MMKV对象,并且让实例化好的Java层MMKV对象持有。之后Java层的方法和native层的方法一一映射就能实现一个直接操作native对象的Java对象。
我们再来看看MMKV的mmkvWithID。
感觉上和defaultMMKV有点相似,也是调用native层方法进行初始化,并且让java层MMKV对象持有native层。那么我们可否认为这两个实例化本质上在底层调用同一个方法,只是多了一个id设置呢?
可以看看MMKV.h文件:
这里就能看到上面的推测是正确的,只要是实例化,最后都是调用mmkvWithID进行实例化。默认的mmkv的id就是mmkv.default。Android端则会设置一个默认的page大小,假设4kb为例子。
所有的mmkvID以及对应的MMKV实例都会保存在之前实例化的g_instanceDic散列表中。其中mmkv每一个id对应一个文件的路径,其中路径是这么处理的:
如果发现对应路径下的mmkv在散列表中已经缓存了,则直接返回。否则就会把相对路径保存下来,传递给MMKV进行实例化,并保存在g_instanceDic散列表中。
我们来看看MMKV构造函数中几个关键的字段是怎么初始化。
mmkvID就是经过md5后对应缓存文件对应的路径。
能看到这里是根据当前的mode初始化id,如果不是ashmem匿名共享内存模式进行创建,则会和上面的处理类似。id就是经过md5后对应缓存文件对应的路径。
注意这里mode设置的是MMKV_ASHMEM,也就是ashmem匿名共享内存模式则是如下创建方法:
实际上就是在驱动目录下的一个内存文件地址。
接下来,在构造函数中使用了共享的文件锁进行保护后,调用loadFromFile进一步的初始化MMKV内部的数据。
我们大致的了解MMKV中每一个字段的负责的职责,但是具体如何进行工作下文都会解析。
在这里面我们遇到了看起来十分核心的类MemoryFile,它的名字有点像 Ashmem匿名共享内存 一文中描述过Java层的映射的匿名内存文件。
我们先来看看MemoryFile的初始化。
MemeoryFile分为两个模式进行初始化:
这里的处理很简单:
能看到此时将会调用mmap系统调用,通过设置标志位可读写,MAP_SHARED的模式进行打开。这样就file就在在内核中映射了一段4kb内存,以后访问文件可以不经过内核,直接访问file映射的这一段内存。
关于mmap系统调用的源码解析可以看这一篇 Binder驱动的初始化 映射原理 。
能看到在这个过程中实际上还是通过ftruncate进行扩容,接着调用zeroFillFile,先通过lseek把指针移动当前容量的最后,并把剩余的部分都填充空数据'\0'。最后映射指向的地址是有效的,会先解开后重新进行映射。
为什么要做最后这个步骤呢?如果阅读过我解析的mmap的源码一文,实际上就能明白,file使用MAP_SHARED的模式本质上是给file结构体绑定一段vma映射好的内存。ftruncate只是给file结构体进行了扩容,但是还没有对对应绑定虚拟内存进行扩容,因此需要解开一次映射后,重新mmap一次。
MMKV在如果使用Ashmem模式打开:
接下来loadFromFile 这个方法可以说是MMKV的核心方法,所有的读写,还是扩容都需要这个方法,从映射的文件内存,缓存到MMKV的内存中。
进入到这个方法后进行如下的处理:
在这里,遇到了一个比较有歧义的字段m_version ,从名字看起来有点像MMKV的版本号。其实它指代的是MMKV当前的状态,由一个枚举对象代表:
注意m_vector是一个长度16的char数组。其实很简单,就是把文件保存的m_vector获取16位拷贝到m_metaInfo的m_vector中。因为aes的加密必须以16的倍数才能正常运作。
初始化分为这6点,我们从最后三点开始聊聊MMKV的初始化的核心逻辑。我们还需要开始关注MMKV中内存存储的结构。
能看到首先从m_file获取映射的指针地址,往后读取4位数据。这4位数据就是actualSize 真实数据。但是如果是m_metaInfo的m_version 大于等于3,则获取m_metaInfo中保存的actualSize。
其校验的手段,是通过比较m_metaInfo保存的crcDigest和从m_file中读取的crcDigest进行比较,如果一致说明数据无误,则返回true,设置loadFromFile为true。
其实这里面只处理m_metaInfo的m_version的状态大于等于3的状态。我们回忆一下,在readActualSize方法中,把读取当前存储的数据长度,分为两个逻辑进行读取。如果大于等于3,则从m_metaInfo中获取。
crc校验失败,说明我们写入的时候发生异常。需要强制进行recover恢复数据。
首先要清除crc校验校验了什么东西:
MMKV做了如下处理,只处理状态等级在MMKVVersionActualSize情况。这个情况,在m_metaInfo记录上一次MMKV中的信息。因此可以通过m_metaInfo进行校验已经存储的数据长度,进而更新真实的已经记录数据的长度。
最后读取上一次MMKV还没有更新的备份数据长度和crc校验字段,通过writeActualSize记录在映射的内存中。
如果最后弥补的校验还是crc校验错误,最后会回调onMMKVCRCCheckFail这个方法。这个方法会反射Java层实现的异常处理策略
如果是OnErrorRecover,则设置loadFromFile和needFullWriteback都为true,尽可能的恢复数据。当然如果OnErrorDiscard,则会丢弃掉所有的数据。
Android-ViewPager源码解析与性能优化
参考技术A ViewPager高度设置为wrap_content或者具体的高度值无效,是因为ViewPager的onMeasure方法在度量宽高的时候,在方法体的最开始就直接调用了setMeasuredDimension()方法将自身的宽高度量,但是并没有在其onMeasure()计算完其具体的子View的宽高之后,重新度量一次自身的宽高从这里我们可以看到,ViewPager的宽高会受其父容器的宽高的限制,但是并不会因为自身子View的宽高而影响ViewPager的宽高。
看setMeasuredDimension的源码调用可以看出,当父容器的高度确定时,ViewPager的宽高其实就是父容器的宽高,ViewPager就是在onMeasure方法一进来的时候就直接填充满整个父容器的剩余空间。在计算孩子节点之前,就已经计算好了ViewPager的宽高,在计算完孩子节点之后,并不会再去重新计算ViewPager的宽高。
自定义一个ViewPager,根据子View的宽高重新度量ViewPager的宽高。其实做法就是在自定义onMeasure的super.onMeasure(widthMeasureSpec, heightMeasureSpec);之前重新计算heightMeasureSpec,将原本ViewPager接收的父容器的限定的heightMeasureSpec替换成我们自定义的heightMeasureSpec。
但是这样的做法,会有种问题,即在ViewPager的子View是采用LinearLayout作为根布局的时候,并且给LinearLayout设置了固定的高度值,那么会出现ViewPager动态高度无效的问题
其实具体的做法,就是仿造measureChild的做法,自定义子View的heightMeasureSpec然后度量整个子View,其实子View的宽度也可以这样做。
这里其实是源码层做了限制,在setOffscreenPageLimit中设置了一个默认值,而这个默认值的大小为1
所以从这里可以看出,ViewPager的最小缓存的limit是1,而不能小于1,当小于1的时候就会被强制的设置为1。
而populate()函数就是用来处理ViewPager的缓存的。
populate()的生命周期是与Adapter的生命周期绑定的。
其实在setOffscreenPageLimit()的时候,调用的populate(),而populate()内部调用的
而pupulate(int newCurrentItem)方法在另一处调用的地方就是在setCurrentItem。
其实ViewPager缓存都是基于ItemInfo这个类来进行的,
看下ViewPager.addNewItem的源码
其实ViewPager.addNewItem就是通过调用Adapter.instantiateItem来创建对应的View,并且将View保存到ItemInfo中的object属性,并且判断ViewPager缓存中是否已经有ItemInfo,如果没有,则添加,如果有则做修改替换
从分析FragmentStatePagerAdapter来看,setUserVisibleHint方法会优先于Fragment的生命周期函数 执行。因为在FragmentStatePagerAdapter中提交事务,是在调用finishUpdate方法中进行的,只有提交事务的时候,才会去执行Fragment的生命周期。
FragmentStatePagerAdapter中的instantiateItem和destroyItem都实现了对fragment的事务的添加和删除,而finishUpdate实现了事务的提交,所以在实现FragmentStatePagerAdapter的时候,并不需要重写instantiateItem和destroyItem
以上是关于存储性能优化 MMKV源码解析的主要内容,如果未能解决你的问题,请参考以下文章
Android MMKV - 性能强悍的存储工具(腾讯出品)