Android 轻量级存储方案的前世今生,kotlin处理null异常

本文深入探讨了Android中SharedPreferences的使用问题,包括内存占用、ANR风险和多进程安全性。提出了MMKV作为轻量级存储方案的优势,如高性能、实时写入和跨进程支持。MMKV通过mmap技术和protobuf序列化提供了高效的数据存储,并支持从SharedPreferences平滑迁移。此外,文章介绍了DataStore作为新的存储选项,强调了其协程和Flow支持的特点,以及与SharedPreferences的对比。
摘要由CSDN通过智能技术生成

sp.edit().putString(“c”, “dmn”).apply();

每次调用 edit 方法都会创建一个 Editor 对象,造成额外的内存占用。很多设计者会对 SharedPreferences 进行封装,隐藏掉 edit()commit/apply()调用流程,但往往同时也忽略了Editor.commit/apply()的设计理念和使用场景。如果是复杂的场景,用户可以在多次 putXxx 方法之后再统一进行 commit/apply(),也就是一次更新多个键值对,只进行一次 IO 操作。

commit/apply 引起的 ANR 问题

commit 是同步地提交到硬件磁盘,有返回值表明修改是否成功,如果在主线程中提交会阻塞线程,影响后续的操作,可能导致 ANR;而 apply 是将修改数据提交到内存,而后异步真正提交到硬件磁盘,没有返回值。我们着重研究一下 apply 为什么会导致 ANR 问题,先来看看 apply 的源码:

@Override

public void apply() {

final long startTime = System.currentTimeMillis();

final MemoryCommitResult mcr = commitToMemory();

final Runnable awaitCommit = new Runnable() {

@Override

public void run() {

try {

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

} catch (InterruptedException ignored) {

}

···

}

};

QueuedWork.addFinisher(awaitCommit); //加入队列

Runnable postWriteRunnable = new Runnable() {

@Override

public void run() {

awaitCommit.run();

QueuedWork.removeFinisher(awaitCommit);

}

};

SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

notifyListeners(mcr);

}

首先把带有 await 的 runnable 添加到 QueuedWork 队列,然后把这个写入任务 postWriteRunnable 通过 enqueueDiskWrite 交给 HandlerThread(Handler + Thread) 进行执行,待处理的任务排队进行执行。然后我们进入 ActivityThread 的 handleStopActivity 方法中,可以看到如下代码

// Make sure any pending writes are now committed.

if (!r.isPreHoneycomb()) {

QueuedWork.waitToFinish();

}

我们再来看看 waitToFinish 中的一段源码

Is called from the Activity base class’s onPause(), after BroadcastReceiver’s onReceive,

  • after Service command handling, etc. (so async work is never lost)

*/ //这个注释很重要

public static void waitToFinish() {

···

try {

while (true) {

Runnable finisher;

synchronized (sLock) {

finisher = sFinishers.poll();

}

if (finisher == null) {

break;

}

finisher.run(); //关键,相当于调用 mcr.writtenToDiskLatch.await()

}

} finally {

sCanDelay = true;

}

}

还记得之前的 QueuedWork.addFinisher(awaitCommit)吗,里面的 awaitCommit 在等待写入线程,如果用户使用了太多的 apply,也就是说写入队列中会有很多写入任务。而只有一个线程在写入,一旦涉及到大量的读写很容易造成ANR(android 8.0 之前,android 8.0 之前的实现 QueuedWork.waitToFinish 是有缺陷的。在多个生命周期方法中,在主线程等待任务队列去执行完毕,而由于cpu调度的关系任务队列所在的线程并不一定是处于执行状态的,而且当apply提交的任务比较多时,等待全部任务执行完成,会消耗不少时间,这就有可能出现 ANR),因为本文的源码时基于 android 29 的,所以该版本或者说是 android 8.0之后并不存在 ANR 问题,因为 8.0之后做了很大的优化,会主动触发processPendingWork取出写任务列表中依次执行,而不是只在在等待。还有一个更重要的优化

我们知道在调用 apply 方法时,会将改动同步提交到内存中 map 中,然后将写入磁盘的任务加入的队列中,在工作线程中从队列中取出写入任务,依次执行写入。注意,不管是内存的写入还是磁盘的写入,对于一个 xml 格式的 sp 文件来说,都是全量写入的。 这里就存在优化的空间,比如对于同一个 sp 文件,连续调用 n 次apply,就会有 n 次写入磁盘任务执行,实际上只需要最后执行最后那次就可以了,最后那次提交对应内存的 map 是持有最新的数据,所以就可以省掉前面 n-1 次的执行,这个就是android 8.0中做的优化,是使用版本来进行控制的。

解决方案

解决方案可以参考今日头条的解决方案,通过反射 ActivityThread 中的 H(Handler) 变量,给 Handler 设置一个 callback,Handler 的 dispatchMessage 中先处理 callback。队列清理需要反射调用 QueuedWork。Google 之所以在Activity/Service 的 onStop 之前调用该方法是为了尽量保证 sp 的数据持久化,该文章中也对比了清理队列和未清理情况下的失败率(相差不大)。

还有一个解决方案,因为 SharedPreferences 是个接口,所以可以自己实现 apply (异步调用系统 commit,这样并不会导致类似系统 apply 那样的阻塞),同时重写 Activity 和 Application 的 getSharedPreference 方法,直接返回自己的实现。但是这个方案带来的副作用比清理等待锁更加明显:系统apply是先同步更新缓存再异步写文件,调用方在同一线程内读写缓存是同步的,无需关心上下文数据读写同步问题;commit

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值