写点什么

再见 SharedPreferences,你好 MMKV!,大厂面试必备技能

作者:嘟嘟侠客
  • 2021 年 11 月 28 日
  • 本文字数:2995 字

    阅读完需:约 10 分钟

不过也带来很多问题,尤其是由 SP 引起的 ANR 问题,非常常见。


正因如此,后来也出现了一些 SP 的替代解决方案,比如 MMKV。


本文主要包括以下内容


  1. SharedPreferences 存在的问题

  2. MMKV 的基本使用与介绍

  3. MMKV 的原理


SharedPreferences 存在的问题


==================================================================================


SP 的效率比较低


  1. 读写方式:直接 I/O

  2. 数据格式:xml

  3. 写入方式:全量更新



由于 SP 使用的 xml 格式保存数据,所以每次更新数据只能全量替换更新数据。


这意味着如果我们有 100 个数据,如果只更新一项数据,也需要将所有数据转化成 xml 格式,然后再通过 io 写入文件中。


这也导致 SP 的写入效率比较低。


commit 导致的 ANR


public?boolean?commit()?{


//?在当前线程将数据保存到 mMap 中


MemoryCommitResult?mcr?=?commitToMemory();


SharedPreferencesImpl.this.enqueueDiskWrite(mcr,?null);


try?{


//?如果是在 singleThreadPool 中执行写入操作,通过 await()暂停主线程,直到写入操作完成。


// commit 的同步性就是通过这里完成的。


mcr.writtenToDiskLatch.await();


}?catch?(InterruptedException?e)?{


return?false;


}


/*


*?回调的时机:


*?1.?commit 是在内存和硬盘操作均结束时回调


*?2.?apply 是内存操作结束时就进行回调


*/


notifyListeners(mcr);


return?mcr.writeToDiskResult;


}


如上所示


  1. commit 有返回值,表示修改是否提交成功。

  2. commit 提交是同步的,直到磁盘操作成功后才会完成。


所以当数据量比较大时,使用 commit 很可能引起 ANR。


Apply 导致的 ANR


commit 是同步的,同时 SP 也提供了异步的 apply。


apply 是将修改数据原子提交到内存, 而后异步真正提交到硬件磁盘, 而 commit 是同步的提交到硬件磁盘,因此,在多个并发的提交 commit 的时候,他们会等待正在处理的 commit 保存到磁盘后在操作,从而降低了效率。


而 apply 只是原子的提交到内容,后面有调用 apply 的函数的将会直接覆盖前面的内存数据,这样从一定程度上提高了很多效率。


但是 apply 同样会引起 ANR 的问题。


public?void?apply()?{


final?long?startTime?=?System.currentTimeMillis();


final?MemoryCommitResult?mcr?=?commitToMemory();


final?Runnable?awaitCommit?=?new?Runnable()?{


@Override


public?void?run()?{


mcr.writtenToDiskLatch.await();?//?等待


......


}


};


//?将?awaitCommit?添加到队列?QueuedWork?中


QueuedWork.addFinisher(awaitCommit);


Runnable?postWriteRunnable?=?new?Runnable()?{


@Override


public?void?run()?{


awaitCommit.run();


QueuedWork.removeFinisher(awaitCommit);


}


};


SharedPreferencesImpl.this.enqueueDiskWrite(mcr,?postWriteRunnable);


}


  • 将一个?awaitCommit?的?Runnable?任务,添加到队列?QueuedWork?中,在 awaitCommit 中会调用?await()?方法等待,在?handleStopService?、handleStopActivity?等等生命周期会以这个作为判断条件,等待任务执行完毕。

  • 将一个?postWriteRunnable?的?Runnable?写任务,通过 enqueueDiskWrite?方法,将写入任务加入到队列中,而写入任务在一个线程中执行。


为了保证异步任务及时完成,当生命周期处于?handleStopService()?、handlePauseActivity()?、?handleStopActivity()?的时候会调用 QueuedWork.waitToFinish()?会等待写入任务执行完毕。


private?static?final?ConcurrentLinkedQueue<Runnable>?sPendingWorkFinishers?=


new?ConcurrentLinkedQueue<Runnable>();


public?static?void?waitToFinish()?{


Runnable?toFinish;


while?((toFinish?=?sPendingWorkFinishers.poll())?!=?null)?{


toFinish.run();?//?相当于调用?mcr.writtenToDiskLatch.await()?方法


}


}


  • sPendingWorkFinishers?是?ConcurrentLinkedQueue?实例,apply?方法会将写入任务添加到?sPendingWorkFinishers 队列中,在单个线程的线程池中执行写入任务,线程的调度并不由程序来控制,也就是说当生命周期切换的时候,任务不一定处于执行状态。

  • toFinish.run()?方法,相当于调用?mcr.writtenToDiskLatch.await()?方法,会一直等待。

  • waitToFinish()?方法就做了一件事,会一直等待写入任务执行完毕,其它什么都不做,当有很多写入任务,会依次执行,当文件很大时,效率很低,造成 ANR 就不奇怪了。


所以当数据量比较大时,apply 也会造成 ANR。


getXXX() 导致 ANR


不仅是写入操作,所有?getXXX()?方法都是同步的,在主线程调用?get?方法,必须等待 SP 加载完毕,也有可能导致 ANR。 调用?getSharedPreferences()?方法,最终会调用 SharedPreferencesImpl#startLoadFromDisk()?方法开启一个线程异步读取数据。


private?final?Object?mLock?=?new?Object();


private?boolean?mLoaded?=?false;


private?void?startLoadFromDisk()?{


synchronized?(mLock)?{


mLoaded?=?false;


}


new?Thread("SharedPreferencesImpl-load")?{


public?void?run()?{


loadFromDisk();


}


《Android 学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》

【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享


}.start();


}


正如你所看到的,开启一个线程异步读取数据,当我们正在读取一个比较大的数据,还没读取完,接着调用?getXXX()?方法。


public?String?getString(String?key,?@Nullable?String?defValue)?{


synchronized?(mLock)?{


awaitLoadedLocked();


String?v?=?(String)mMap.get(key);


return?v?!=?null???v?:?defValue;


}


}


private?void?awaitLoadedLocked()?{


......


while?(!mLoaded)?{


try?{


mLock.wait();


}?catch?(InterruptedException?unused)?{


}


}


......


}


在同步方法内调用了?wait()?方法,会一直等待?getSharedPreferences()?方法开启的线程读取完数据才能继续往下执行,如果读取几 KB 的数据还好,假设读取一个大的文件,势必会造成主线程阻塞。


MMKV 的使用


===================================================================


MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。从 2015 年中至今在微信上使用,其性能和稳定性经过了时间的验证。近期也已移植到 Android / macOS / Win32 / POSIX 平台,一并开源。


MMKV 优点


  1. MMKV 实现了 SharedPreferences 接口,可以无缝切换。

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

  3. MMKV 数据序列化方面选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现。

  4. SP 是全量更新,MMKV 是增量更新,有性能优势。


详细的使用细节可以参考文档:https://github.com/Tencent/MMKV/wiki


MMKV 原理


==================================================================


IO 操作


我们知道,SP 是写入是基于 IO 操作的,为了了解 IO,我们需要先了解下用户空间与内核空间


虚拟内存被操作系统划分成两块:用户空间和内核空间,用户空间是用户程序代码运行的地方,内核空间是内核代码运行的地方。为了安全,它们是隔离的,即使用户的程序崩溃了,内核也不受影响。


最后

下面是有几位 Android 行业大佬对应上方技术点整理的一些进阶资料。希望能够帮助到大家提升技术



高级 UI,自定义 View


UI 这块知识是现今使用者最多的。当年火爆一时的 Android 入门培训,学会这小块知识就能随便找到不错的工作了。


不过很显然现在远远不够了,拒绝无休止的 CV,亲自去项目实战,读源码,研究原理吧!



本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

用户头像

嘟嘟侠客

关注

还未添加个人签名 2021.03.19 加入

还未添加个人简介

评论

发布
暂无评论
再见SharedPreferences,你好MMKV!,大厂面试必备技能