导航:首页 > 源码编译 > 源码缺陷实例分析

源码缺陷实例分析

发布时间:2024-06-15 06:17:39

‘壹’ 存储性能优化 MMKV源码解析

好久没有更新常用的第三方库了。让我们来聊聊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,则会丢弃掉所有的数据。

‘贰’ 绋嫔簭璁捐$己闄峰垎鏋愪笌瀹炶返鍓嶈█

褰撴彁鍒扮▼搴忚捐★纴澶у氭暟浜哄瑰叾骞朵笉闄岀敓锛岃嚜璁や负绮鹃氭ら亾銆傜劧钥岋纴阒呰绘湰涔﹀悗锛屼綘浼氩彂鐜帮纴鍗充究鏄镊璁や负楂樻坠镄勪綘锛岃捐$殑绋嫔簭涔熷彲鑳介殣钘忕潃浼楀氭湭瀵熻夌殑缂洪櫡銆傝繖浜涚己闄峰线寰瀵艰嚧杞浠惰繍琛岄梾棰橀戝彂锛屽奖鍝岖敤鎴蜂綋楠岋纴鐢氲呖鍗卞强鐢熷懡璐浜у畨鍏锛岃繖鏄杞浠跺紑鍙戦嗗烟闀挎湡闱涓寸殑鎸戞垬銆

镊杞浠跺嵄链轰互𨱒ワ纴杞浠跺伐绋嫔笀浠涓鐩村湪瀵绘眰鎻愬崌杞浠惰川閲忕殑绛栫暐锛屾彁鍑轰简涓绯诲垪瑙e喅鏂规堛傜劧钥岋纴鐢变簬缂洪櫡镄勫嶆潅镐у拰闅愯棌镐э纴璁稿氱湅浼兼e父镄勪唬镰佷腑鍙鑳介殣钘忕潃娼滃湪镄勯庨橹銆傛湰涔︿互绋嫔簭璁捐$己闄蜂负镙稿绩锛屼緷镓树綔钥呬赴瀵岀殑杞浠舵祴璇旷粡楠岋纴鏁寸悊浜嗘祴璇曚腑鎻绀虹殑椴滀负浜虹煡镄勯梾棰桡纴镞ㄥ湪甯锷╄昏呮繁鍏ョ悊瑙e苟阆垮厤杩欑被闂棰樸

链涔﹀垎涓轰簲涓绔犺妭锛屽唴瀹硅﹀敖銆傜涓绔犵潃閲崭簬闱欐佹祴璇曪纴娑电洊浜嗘枃妗e℃煡銆佷唬镰佸℃煡銆佹妧链璇勫″拰浠g爜璧版煡绛夋柟娉曪纴骞跺归儴鍒嗘柟娉曡繘琛屼简瀵规瘆銆傜浜屻佷笁绔犳槸镙稿绩鍐呭癸纴璇︾粏瑙f瀽浜咰/C++鍜孞ava璇瑷绋嫔簭鍦ㄧ紪镰侀庢牸銆佸唴瀛樼$悊銆佺紦鍐插尯浣跨敤銆佹寚阍埚拰浠g爜瀹夊叏绛夋柟闱㈢殑甯歌侀梾棰桡纴骞堕氲繃瀹炰緥鍒嗘瀽鍜屾彁渚涗慨鏀瑰缓璁銆

绗锲涚珷鎺㈣ㄤ简杞浠惰川閲忕殑闱欐佸害閲忥纴浠嬬粛浜嗗寘𨰾琀alstead搴﹂噺鍜孧cCabe搴﹂噺鍦ㄥ唴镄勪簲绉嶈蒋浠惰川閲忔ā鍨嬨傜浜旂珷鍒欑粨钖埚疄闄呮祴璇曞疄璺碉纴璁茶В浜哖olyspace銆并lockwork銆乀estbed鍜孧cCabe浠g爜闱欐佸垎鏋愬伐鍏风殑浣跨敤鏂规硶锛屼负瀹炶返钥呮彁渚涗简鍏蜂綋宸ュ叿镄勮繍鐢ㄦ寚鍗椼

阒呰绘湰涔︼纴镞犺烘槸杞浠舵祴璇曚汉锻樿缮鏄绋嫔簭璁捐¤咃纴閮藉皢浠庝腑鍙楃泭镩澶氥傚湪姝わ纴鐗瑰埆镒熻阿寮犳棴杈夈佹潹璞广佹潕鍗庤幑銆佽儭鍏㈢帀銆佽懀鏄曘佷簬闀块捄钖屽织鍦ㄧ紪鍐栾繃绋嬩腑镄勮础鐚锛屼粬浠镄勪粯鍑轰负璇昏呮彁渚涗简瀹濊吹镄勬寚瀵笺

阅读全文

与源码缺陷实例分析相关的资料

热点内容
安卓上的谷歌地图怎么用 浏览:181
安卓命令行打包 浏览:514
编程文字与数字教学视频 浏览:815
如何看手机号码注册哪些app 浏览:411
linux查看总内存 浏览:850
python进程间共享 浏览:436
js如何获取本地服务器地址 浏览:68
gfx什么时候支持安卓十一系统 浏览:939
压缩机90兆帕 浏览:928
程序员调侃语句 浏览:579
不是php函数的是 浏览:998
压缩文件好处 浏览:785
3d266期神童三胆计算法 浏览:189
通过爱思助手怎么下载app 浏览:323
vi命令将文件创在桌面上 浏览:923
程序员做竞价 浏览:696
江苏中小学编程纳入课程 浏览:730
单纯形法包括动态规划算法 浏览:951
cpdf百度网盘 浏览:671
综合布线工程中配线架的算法 浏览:926