正文

这是网上找到的一份Android键值对存储方法的功能测验比照(数越小越好):

image.png
能够看出,DataStore的功能比MMKV差了一大截。MMKV是腾讯在2018年推出的,而DataStore是Android官方在2020年推出的,并且它的正式版在2021年8月才发布。一个官方发布的、更新新的库,功能居然比不过SharedPreferences。Android官方当初之所以推出DataStore,便是要代替掉SharePreference,并且主要原因之一便是SharedPreferences有功能问题,可是测验成果却是它的功能不如SharedPreferences.

所以,这到底是为什么?

SharePreference:不知不觉被嫌弃

键值对的存储在移动开发里十分常见。比方深色模式的开关、软件言语、字体大小,这些用户偏好设置,很合适对键值对来存储。而键值对的存储计划,最传统也最广为人知的便是Android自带的SharedPreferences。它里边的-Preferences,便是偏好设置的意思,从姓名也能看出它开始的定位。

SharedPreferences运用起来很简略,也没什么问题,咱们就这么用了许多年。——但!渐渐有人发现它有一个问题:卡顿,乃至有时分会呈现ANR。

MMKV:好快!

怎样办?换!2018年9月,腾讯开源了一个叫做MMKV的项目。它和SharedPreferences相同,都是做键值对存储,可是它的功能比SharedPreferences强许多。真的是强,很,多。在MMKV推出之后,许多团队都把键值对存储计划从SharedPreferences换到了MMKV。

DataStore:官方造废物

再然后,便是又过了两年,Google自己也表示受不了SharedPreferences了,Android团队发布了Jetpack的新库:DataStore,方针直指SharedPreferences,宣称它便是Android官方给出的SharedPreferences的代替品。

image.png
代替的理由,Android团队列了好几条,但不出咱们预料地,「功能」是其中之一:

image.png

也便是说,Android团队直接抛弃了SharedPreferences,换了个新东西来供给更优的功能。

可是,问题随之就呈现了:咱们已测验,发现这个DataStore的功能并不强啊?跟MMKV比起来差远了啊?要知道,MMKV的发布是比DataStore早两年的。DataStore比人家晚两年发布,可是功能却比人家差一大截?乃至,从测验数据来看,它连要被代替掉的SharedPreferences都比不过。这么弱?那它搞个毛啊!

Android团队吭哧吭哧搞个新东西迟来,居然还没有市场上两年前就呈现的东西强?这是为啥?

首先,必定得排除「DataStore是废物」这个或许性。

那假如不是的话,那又是为什么?——由于你被骗了。

MMKV的一二三四

被谁骗了?不是被MMKV骗了,也不是详细的某各人。事情其实是这样的:
咱们知道MMKV当初为什么会被发明出来吗?其实不是为了代替SharedPreferences.

最早是由于微信的一个需求:

微信作为一个全民的谈天App,对话内容的特殊字符所导致的程序溃散是一类很常见、也很需求快速处理的问题;而哪些字符会导致程序溃散,是无法预知的,只能等用户手机上的微信溃散之后,再利用相似时光倒流的回溯行为,看看前次软件溃散的终究一瞬间,用户收到或许发出了什么音讯,再用这些音讯中的文字去测验复现发生过的溃散,终究试出有问题的字符,然后针对性处理。

1.gif

那么这个「时光倒流」应该怎样做,就成了问题的要害。咱们要知道,程序中的一切变量都是存活在内存里的,一旦程序溃散,一切变量全都灰飞烟灭。

2.gif

所以要想完结「时光倒流」,就需求把想回溯的时光预先记载下来。说人话便是,咱们需求把界面里显现的文字写到手机磁盘里,才干在程序溃散、从头发动之后,通过读取文件的方法检查。

3.gif

更费事的是,这种记载的方针是用来回溯查找「导致程序溃散的那段文字」,而一起,正是由于没有人知道哪段文字会导致溃散采纳做的记载,这就要求每一段文字都需求先写入磁盘、然后再去显现,这样才干确保程序溃散的时分那段导致溃散的文字必定已经被记载到了磁盘。

5.gif

对吧?
这就有点难了。

咱们来幻想一下实践场景:

  • 假如用户的微信现在处于一个对话界面中,这时分来了一条新的音讯,这条音讯里或许会包括微信处理不了的字符,导致微信的溃散。

6.gif

  • 而微信为了及时地找出导致溃散的字符串或许字符,所以给程序加了逻辑:一切的对话内容在显现之前,先保存到磁盘再显现:
val bubble: WxTextView = ...
recordTextToDisk(text)// 显现之前,先保存到磁盘
bubble.setText(text)
  • 那么你想一下,这个「保存到磁盘」的行为,我应该做成同步的仍是异步的?

7.gif

  • 为了不卡住主线程,我显然应该做成异步的;
  • 但这是马上就要显现的文字,假如做成异步的,就极有或许在程序溃散的时分,后台线程还没来得及把文字存到磁盘。这样的话,就无法进行回溯,然后这种记载也就失去了价值。

8.gif

  • 所以从或许性的视点来看,我只能挑选放弃功能,把它做成同步的,也便是在主线程进行磁盘的写操作。

10.gif

  • 一次磁盘的写操作,花个一两毫秒是很正常的,三五毫秒乃至超越10毫秒也都是有或许得。详细的计划能够挑选SharedPreferences,也能够挑选数据库,但不论选哪个,只需在主线程去完结这个写操作,这种耗时就肯定无法防止。一帧的时刻也便是16毫秒罢了——那时分还没有高刷,咱们就先不谈高刷了,一帧便是16毫秒里来个写磁盘的的操作,用户很或许就会感受到一次卡顿。
  • 这仍是相比照较好的情况。咱们再想一下,假如用户点开了一个活跃的群,这个群里有几百条没看过的音讯:
    • 那么在他点开的一瞬间,是不是界面中会显现出好几条音讯气泡?这几条音讯的内容,哪些需求记载到磁盘?全都要记载的,由于谁也不知道哪一条会导致微信的溃散,任何一条都是或许得。
    • 而假如把这几条音讯都记载下来,是不是每条音讯的记载都会触及一次写磁盘的操作?这几回写磁盘行为,是发生在同一帧里的,所以在这一帧里由于记载文字而导致的主线程耗时,也会相比起方才的例子翻上好几倍,卡顿时刻就相同也会翻上好几倍。
  • 还有更差的情况。假如用户按完这一页之后,决议翻翻谈天记载,看看咱们之前都聊了什么:
    • 这时分,是不是上方每一个新的谈天气泡的呈现,都会触及一次主线程上的写磁盘行为?
    • 而假如用户把手猛地往下一滑,让上面的几十条音讯依次滑动显现出来,这是不是就会导致一次爆发性的、集中式的对磁盘的写入?
    • 用户的手机,必定会卡爆。
      所以这种「高频、同步写入磁盘」的需求,让一切的现有计划都变得不行行了:不论你是用SharedPreferences仍是用数据库仍是别的什么,只需你在主线程同步写入磁盘,就必定会卡,并且是很卡。

可是微信仍是有高手,仍是有能想办的人,终究微信找到了处理计划。他们没有用任何的线程计划,而是运用了一种叫做内存映射mmap())的底层方法。

image.png

它能够让体系为你指定的文件拓荒一块专用的内存,这块内存和文件之间是主动映射、主动同步地关系,你对文件的改动会主动写到这块内存里,对这块内存的改动也会主动写到文件里。

11.gif

更多更深的原理,说实话我也不是看得很懂,就不跟咱们装了。但要害是,有了这一层内存作为中间人,咱们就能够用「写入内存」的方法来完结「写入磁盘」的方针嘞。内存的速度多快呀,耗时简直能够疏忽,这样一下子就把鞋磁盘形成卡顿的问题处理了。

12.gif

并且这个内存映射还有一点很便利的是,尽管这块映射的内存不是实时向对应的文件写入新数据,可是它在程序溃散的时分,并不会随着进程一起被销销毁,而是会持续有条有理地把它里边还没同步完的内容同步到它所映射的文件里边去。

13.gif
至于更基层原理,我也说了,没看懂,你也别问我。

总归,有了这些特性,内存映射就能够让程序用往内存里写数据的速度完结往磁盘里写数据的实践效果,这样的话,「高频、同步写入磁盘」的需求就完美满足了。不论是用户翻开新的谈天页面,仍是滑动谈天记载来检查谈天前史,用内存映射的方法都能够既实时写入一切即将渲染的文字,又不会形成界面的卡顿。这种功能,是SharedPreferences和数据库都做不到的——趁便提一句,尽管我总在提SharedPreferences,但其实这种做法本来是现在iOS版的微信里应用的,后来才移植到了Android版微信。这也是我方才说的,MMKV的诞生并不是为了代替SharedPreferences.

再后来,便是2018年,微信把这个叫做MMKV的项目开源了。它的姓名,我才九十直白的「Memory-Map based Key-Value(计划)」,基于内存映射的键值对。

在MMKV开源之后,许多团队就把键值对存储计划从SharedPreferences搬迁到了MMKV。为什么呢?由于它快呀。

MMKV并不总是快如闪电

不过……事情其实没那么简略。MMKV尽管大的定位方向和SharedPreferences相同,都是关于键值对的存储,但它并不是一个全方位更优的计划。

比方功能。我前面一直在说MMKV的功能更强,对吧?但现实上,它并不是任何时分都更强。由于内存映射这种计划是自行办理一块独立的内存,所以它的尺度的弹性上面就比较受限,这就导致它在写大一点的数据的时分,速度会慢,并且或许会很慢。我做了一份测验:

14.gif

在接连1000次写入Int值的场景中,SharedPreferences的耗时是1034毫秒,也便是1秒多一点;而MMKV只需2毫秒,简直快得离谱;并且最离谱的是,Android官方最新推出的DataStore是1215毫秒,居然比SharedPreferences还慢。这个前面我也提过,别人的测验也是这样的成果。

可是,SharedPreferences是有异步的API的,而DataStore是基于协程的。这就意味着,它们实践占用主线程的时刻是能够低于这份测验出的时刻的,而界面的流畅介意的是主线程的时刻消耗。所以假如我计算的不是全部的耗时,而是主线程的耗时,那么计算出的SharedPreferencesDataStore得耗时将会大幅度缩减:

15.gif

仍是比MMKV慢许多,是吧?可是这是关于Int类型的高频写入,Int数据是很小的。而假如我把写入的内容换成长字符串,再做一次测验:

16.gif

MMKV就不具备优势了,反而成了耗时最久的;而这时分的冠军就成了DataStore,并且是遥遥领先。这也便是我最初说的:你或许被骗了。被谁骗了?被「耗时」这个词:咱们重视功能,考量的当然是耗时,但要明确:是主线程的耗时。所以视频最初的那张图,是不具备任何参阅意义的。

image.png

但其实,它们都够快了

不过在换成了这种只看主线程的耗时的比照计划之后,咱们会发现谁是冠军其实并不是很重要,由于从终究的数据来看,三种计划都不是很慢。尽管这半秒左右的主线程耗时看起来很可怕,可是要知道这是1000次接连写入的耗时,而咱们在真正写程序的时分,怎样会一次性做1000次长字符串的写入?所以真正项目中的键值对写入的耗时,不论你选哪个计划,都会比这份测验成果的耗时少得多的,都少到了能够疏忽的程度,这是要害。

各自的优势和缺点

那……已然它们的耗时都少到了能够疏忽,不便是选谁都行?那倒不是。

MMKV又是:写速度极快

咱们来看一个MMKV官方给出的数据比照图:

image.png

从这张图看来,SharedPreferences的耗时是MKV的挨近60倍。很显着,假如SharedPreferences用异步的API也便是apply()来存活的话,是不或许有这么差的功能的,这个必定是运用同步的commit()的功能来做的比照。那么为什么MMKV官方会这样做比照呢?这个又要提到它的诞生场景了:MMKV开始的功能是在文字显现之前把它记载到磁盘,然后假如接下来这个文字显现失败导致程序溃散,稍后就能够从磁盘里把这段文字康复出来,进行剖析。而方才我也说过,这种场景的特殊性在于,导致程序溃散的文字往往是刚刚被记载下来,程序就溃散了,所以假如选用异步处理的计划,就很有或许在文字还没来得及真正存储到磁盘的时分程序就发生了溃散,那就没办法把它康复出来进行剖析了。因而这样的场景,是不能承受异步处理的计划的,只能同步进行。所以MMKV介意的,便是同步处理机制下的耗时,它不介意异步,由于它不承受异步。

而在同步处理的急之下,MMKV的功能优势就太显着了。原因上面说过了,它写入内存就简直等于写入了磁盘,所以速度巨快无敌。这便是MMKV的优势之一:极高的同步写入磁盘的功能。

别的MMKV还有个特点是,它的更新并不像SharedPreference那样全量从头写入磁盘,而是只需把要更新的键值对写入,也便是所谓的增量式更新。这也会给它带来一些功能优势,不过这个又是并不算太中心,由于SharedPreferences尽管是全量更新模式,但只需把保存的数据用合适的逻辑拆分到多个不同的文件里,全量更新并不会对功能形成太大的拖累。所以这个功能优势尽管有,但并不是要害。

还有方才提到的,关于大字符串的场景,MMKV的写入功能并不算快,乃至在咱们的测验成果里是最慢的,对吧?这一点算是下风。可是实事求是地说,咱们在开发里不太或许接连不断地去写入大字符串吧?所以这个功能下风尽管有,但也并不是要害。

全体来说,MMKV比起SharedPreferences和DataStore来说,在写入小数据的情况下,具有很高的写入功能,这就让高频写入的场景十分合适运用MMKV来处理。因而假如你的项目里也有像微信的溃散回溯的这种高频写入的需求,MMKV就很或许是你的最佳计划。而假如你除了「高频写入」,还和微信相同要求「同步写入」,那MMKV就或许是你的仅有挑选了。不过,假如你真的主要是存储大字符串——例如你写的是一个文本编辑器软件,需求保存的总是大块的文本——那么用MMKV不必定会更快了,乃至或许会比较慢。

MMKV优势:支撑多进程

别的,MMKV还有一个巨大的优势:它支撑多进程。

行业内也有许多公司选用MMKV并不是由于它快,而是由于它支撑多进程。SharedPreferences是不支撑多进程的,DataStore也不支撑——从DataStore提交的代码来看,它已经在参加多进程的支撑了,但目前还没有完结。所以假如你们公司的APp是需求在多进程里访问键值对数据,那么MMKV是你仅有的挑选。

MMKV下风:丢数据

除了速度快和支撑多进程这两个优势之外,MMKV也有一个缺点:它会丢数据。

任何的操作体系、任何的软件,在往磁盘写数据的过程中假如发生了意外——例如程序溃散,或许断电关机——磁盘里的文件就会以这种写了一半的、不完整的形式被保留。写了一半的数据怎样用啊?没法用,这便是文件的损坏。这种问题是不或许防止的,MMKV尽管由于底层机制的原因,在程序溃散的时分不会影响数据往磁盘的写入,但断电关机之类的操作体系等级的溃散,MMKV就没办法了,文件照样会损坏。关于这种文件损坏,SharedPreferences和DataStore的对应方法是在每次写入新数据之前都对现有文件做一次主动备份,这样在发生意外呈现了文件损坏之后,它们就会把备份的数据康复过来;而MMKV,没有这种主动的备份和康复,那么当文件发生了损害,数据就丢了,之前保存的各种信息只能被重置。也便是说,MMKV是仅有丢数据的计划。

或许会有人好奇,为什么MMKV不做全主动的备份和康复。我的猜想是这样的:MMKV底层的原理是内存映射,而内存映射这种方法,它从内存往自盘里同步写入的过程并不是实时的,也便是说并不是每次咱们写入到映射的内存里就会当即从这块内存写入到磁盘,而是会有一些滞后。而假如咱们要做全主动的备份,那就需求每次往内存里写入之后,当即手动把内存里最新的数据同步到磁盘。但这就和MMKV的定位不符了:由于这种「同步」本质上便是一次从内存到磁盘的写入,并且是同步的写入;而MMKV是要高频写入的,假如在高频写入内存的一起,还要实时把数据从内存同步到磁盘,就会一下子把写入速度从内存等级下降到磁盘等级,MMKV的功能优势也就荡然无存了。所以从原理上,主动备份是个很难完结的需求,由于它和MMKV的定位是矛盾的。不过正好MMKV所要记载的这些要显现的文字,也并不是不能丢掉的内容——真要是丢了就丢了呗,横竖是溃散日志,丢了就不要了,我下次发动程序之后记载便是了——所以已然要求有必要高频写入而导致很难完结主动备份,并且也的确能承受由于不做主动备份而导致的数据损坏,那就爽性不做主动备份了。不过这也是我猜的啊。

所以假如你要用MMKV,必定要记得只能用来存能够承受丢掉、不那么重要的数据。或许你也能够挑选对数据进行定期的手动备份——全主动的实时备份应该是会严重影响功能的,不过你没试过,你假如有兴趣能够试试。别的据我所知,国内涵运用MMKV的团队里,简直没有对MMKV数据做了备份和康复的处理的。

那么提到这里,很简略引出一个问题:微信自己就不怕丢数据吗?关于这一点,我信任,微信肯定不会把自己登录状况相关的信息用MMKV保存并且不做任何备份,由于这必定会导致每天都会有一些用户在新一次翻开微信的时分发现自己登出了。这会是十分差的用户体验,所以微信必定不会让这种事发生。至于一些简略的用户设置,那我就不清楚了。比方深色主题重要吗?这是个不好说的事情:某个用户在翻开软件的时分发现自己设置的深色主题失效了,软件忽然变回了亮色计划,这必定是不舒服的事;但咱们要知道,MMKV的文件损坏终归是个概率极低的事情,所以偶尔而地发生一次这样的事情在产品的视点是否能够承受,那或许是需求产品团队自身作一个归纳考量的事儿。关于不同的产品和团队,或许不行承受,或许无伤大雅。而关于你所开发的产品应该是怎样的判别,就得各位自己和团队去商议。所以像深色主题这种「能够重要也能够不重要」的信息,用不用MMKV保存、用的时分做不做备份,咱们需求自己去判别。

总归,咱们要知道这件事:MMKV是有数据损坏的概率,这个在MMKV的官方文档就有说明:MMKV的GitHub wiki页面显现,微信的iOS版平均每天有70万次的数据校验不通过(即数据损坏)。这仍是2020年的数据,现在或许会更多。

image.png

所以咱们在运用MMKV的时分,必定要考虑到这个问题,你要知道这件事。至于详细的应对,是承受它、坏就坏了,仍是要仔细应对、做好备份和康复,这便是咱们自己的决议计划了。

SharedPreferences的优势:不丢数据

好,那么说完了MMKV,我来说一下SharedPreferences,这个最传统的计划。

它有什么优势呢?——它没有优势。跟MMKV比起来,它不会丢数据,这个倒是它比MMKV强的当地,可是我觉得更应该归为MMKV的下风,而不是SharedPreferences的优势,由于只需MMKV会丢掉句嘛,是吧?

不过不论是这个优势仍是那个优势,假如你不希望丢数据,并且也不想花时刻去做手动的备份和康复,一起关于MMKV的超高写入功能以及多进程支撑都没有需求,那你其实更应该挑选SharedPreferences,而不是MMKV。对吧?

SharedPreferences的下风:卡顿

但更进一步地说:假如你挑选了SharedPreferences,那么你更应该考虑DataStore。由于DataStore是一个彻底超越了SharedPreferences的存在。你看SharedPreferences和MMKV它俩是各有好坏对吧?尽管MMKV简直完胜,可是究竟SharedPreferences不会丢数据呀,所以他俩是各有好坏的。但当DataStore和SharedPreferences比起来,那便是DataStore完胜了。这其实也很合理,由于DataStore被发明出来,便是用于代替掉SharedPreferences的;而MMKV不相同,它的诞生有独特的任务,它是为了「高频同步写入」而诞生的,所以不能全视点胜过SharedPreferences也很正常。

咱们还说回DataStore。DataStore被发明出来的方针便是代替SharedPreferences,而它处理的SharedPreferences最大的问题有两点:一是功能问题,二是回调问题

先说功能问题:SharedPreferences尽管能够用异步的方法来保存更改,因而来防止I/O操作所导致的主线程的耗时;但在Activity发动和关闭的时分,Activity会等候这些异步提交完结保存之后再持续,这相当于把异步操作转换成同步操作了,然后会导致卡顿乃至ANR(程序未响应)。这是为了确保数据的一致性而不得不做的决议,但它也的确成为了SharedPreferences的一个缺点。而MMKV和DataStore用不同的方法各自都处理了这个问题——现实上,当初MMKV被发布的时分之所以在业界有相当大的反响,便是由于它处理了SharedPreferences的卡顿和ANR的问题。

不过有一点我的观念或许和一些人不同:SharedPreferences所导致的卡顿和ANR,其实并不是个很大的问题。它和MMKV的数据损坏相同,都是十分低概率的事情。它俩最大的差异在于其实是政治上的:SharedPreferences的卡顿很简略被大公司的功能剖析后台检测到,所以不处理的话会扣绩效,而处理掉它会提高绩效;而MMKV的数据损坏是无法被检测到的,所以……哈?现实上,咱们想一下:卡顿和数据损坏,哪个更严重?当然是数据损坏了,对吧。

其实除了写数据时的卡顿,SharedPreferences在读取数据的时分也会卡顿。 尽管它的文件加载过程是在后台进行的,但假如代码在它加载完结之前就去测验读取键值对,现成就会被卡住,直到文件加载完结,而假如这个读取的过程发生在主线程,就会形成界面卡顿,并且数据文件越大就会越卡。这种卡顿,不是SharedPreferences独有的,MMKV也是存在的,由于它初始化的过程相同也是从磁盘里读取文件,并且是一股脑把整个文件读完,所以耗时并不会比SharedPreferences少。而DataStore,就没有这种问题。DataStore不论是读文件仍是写文件,都是用的协程在后台进行读写,一切的I/O操作都是在后台线程发僧的,所以不论读仍是写,都不会卡住主线程。

简略来说,SharedPreferences会有卡顿的问题,这个问题MMKV处理了一部分(写时的卡顿),而DataStore彻底处理了。所以假如你的方针在于全方位的功能,那么你应该考虑的是DataStore。由于它是仅有彻底不会卡顿的。

SharedPreferences的下风:回调

DataStore处理的SharedPreferences的另一个问题便是回调。SharedPreferences假如运用同步方法来保存更改(commit()),会导致主线程的耗时;但假如运用异步的方法,给它回调又很不便利,也便是假如你想做一些「等这个异步提交完结之后再怎样怎样样」的工作,会很费事。

而DataStore由于是用协程来做,现成的切换是十分简略的,你就把「保存完结之后做什么」直接写在代码的下方就能够了,很直观、很简略。

比照来说,MMKV尽管没有运用协程,可是它太快了,所以大多数时分并不需求切线程也不会卡顿。总归这件事上,只需SharedPreferences最弱。

总结

差异大概便是这些差异了,大致总结一下便是:

假如你有多进程支撑的需求,MMKV是你仅有的挑选;假如你有高频写入的需求,你也应该优先考虑MMKV。但假如你运用MMKV,必定要知道它是或许丢数据的,不过概率很低便是了,所以你要在权衡之后做好决议:是自行完结数据的备份和康复计划,仍是直接承受丢数据的现实,在每次丢数据之后帮用户把对应的数据进行初始化。当然了,一个最鸡贼的做法是:横竖数据检测不会检测到MMKV的数据丢掉,又不影响绩效,那就不论他呗!不过我个人是不赞同这种策略的,有点不负责任哈。

别的,假如你没有多进程的需求,也没有高频写入的需求,DataStore作为功能最完美的计划,应该优先被考虑。由于它在任何时分都不会卡顿,而MMKV在写大字符串和初度加载文件的时分,是或许会卡顿的,并且初度加载文件的卡顿不是概率性的,只需文件大到了引起卡顿的程序,便是100%的卡顿。不过假如你的团队没有在用协程,乃至没有再用Kotlin,那DataStore也暂时不合适你们,由于它是彻底依靠Kotlin协程来完结和运用的。

哦对了,其实我今天说的DataStore只是面向简略键值存储的DataStore计划,它的全称叫Preferences DataStore,而DataStore还用于保存结构化数据的计划,叫做Proto DataStore,它内部用的是Protocol Buffer作为数据结构的支撑。

至于SharedPreferences嘛,在这个时代,它真的能够被放弃了。除非——想我刚说的——假如你们还没有用协程,那SharedPreferences或许还能苟延残喘一下。

版权声明

本文首发于:【面试黑洞】Android 的键值对存储有没有最优解?

微信公众号:扔物线