深入理解SharedPreferences

最近在研究app性能优化,发现自己现在在做的app的线上环境的bugly中发现有些是SharedPreferences造成的app卡顿现象,因此基于这个场景对SharePreferences进行了深入探究,记录如下(本篇源码基于Android7.1.1):

源码解析

初始化

SharePreferences初始化的方式一般分为两种:

  1. COntextImpl.getSharedPreferencesences
  2. PreferenceManager.getDefaultSharedPrefer

我们来分别进入下源码查看下里面的sp是如何初始化的,首先来看第一种:
ContextWrapper.java:
在这里插入图片描述
这里mBase为Context对象,Context作为一个抽象类,我们可以去ContextImpl去查看其实现方法getSharedPreferences(String name, int mode):
在这里插入图片描述
这里首先进行了name==null的判断保护,把文件名字命名为null.xml。然后就是从mSharedPrefsPaths里取出根据name取出file,这里的mSharedPrefPaths相当于一个k-v关系为name和xml文件的file的一个map,从这个map中取出xml文件对应的file对象,然后调用getSharedPreferencs(File file, int mode)方法,这里注意方法加了锁,所以这里是线程安全的。

接下来来看getSharedPreferencs(File file, int mode)方法:
在这里插入图片描述
首先从缓存中读取是否存在对应的SharePreferencesImpl对象,如果为null,则调用其构造方法新建一个SharePreferencesImpl对象并保存到缓存中。

接下来我们再来看SharePreferencesImpl的构造方法:

在这里插入图片描述
这里我们先来解释下构造方法里几个变量,mFile对应的是之前从mSharedPrefsPaths中取出来的对应的xml文件,mBackupFile对应的是一个灾备文件,用于用户写入失败时进行恢复,我们可以看makeBackupFile方法:
在这里插入图片描述
灾备文件相当于给数据做了一层安全性保障。mMode对应的是一开始ContextImpl.getSharePreferences方法里传入的mode值,mLoaded为是否加载完成的标记,用于标记后面从xml文件中读取保存到map中的流程,后面会看到,mMap为我们的sp数据的内存缓存,之后会详述。

接着看startLoadFromDisk方法,开启一个线程,调用loadFromDisk方法,我们接着跟下去:
在这里插入图片描述
这个方法有点长,我们来一点一点分析:
首先如果灾备文件存在,则直接使用灾备文件回滚。
然后就是文件的保护判断,如果文件可读状态的话,则把xml文件读取出来,并且保存到map对象中,接下来就是加锁操作,把map保存到mMap对象中,就是上面提到的sp数据的内存缓存,并且记录下读取文件的时候,把mLoaded标记设置为true,意思就是读取完成,最后调用mLock对象的notifyAll方法通知读取完毕,将其他在等待中的线程激活。

get

讲完初始化之后,我们再来讲get操作,get有很多类型,源码截取如下:
在这里插入图片描述
在这里插入图片描述
我们可以看到get操作大同小异,我们来以getString为例,我们可以看到getString方法加锁,所以是线程安全的,首先调用了awaitLoadedLocked()方法,我们可以看到awaitLoadedLocked方法是在等待配置文件读取完毕,只有读取完毕了才能进行get操作。然后就是从map中根据key来取值,所以这里可以看到sp的get操作全部都是从内存缓存中读取的。

put

put操作我们先来看看用法:

//commit或者apply
mSharedPreferences.edit().putString(key, value).commit();

ok,我们先来看edit方法:
在这里插入图片描述
首先同样首先调用了awaitLoadedLocked方法,然后new了一个EditorImpl对象:
在这里插入图片描述
这里我们看到put操作就是往mModified这个map里放入了要修改的key和value值,这里要特别注意clear方法,只是把mClear这个tag标记设置为true而已,并没有把mModified这个map清空,接着往下看,这里我们先来看apply方法,apply方法看懂了以后看commit方法就很轻松了:

在这里插入图片描述

首先调用了commitToMemory方法获取到了一个MemoryCommitResult对象,我们先来看这个MemoryCommitResult和commitToMemory方法里面怎么操作的:

在这里插入图片描述

在这里插入图片描述
我们可以看到commitToMemory方法直接return一个MemoryCommitResult对象,所以我们先来看commitToMemory方法。
首先,做了一层克隆保护,把mMap赋值给mapToWriteToDisk,看变量名就知道,这个mapToWriteToDisk就是要回写到磁盘的map。如果mClear标记为true,那么就把mMap清空,然后就开始遍历之前put操作里put进去的mModified的这个map,如果value为null,则remove这个key值,不为null,则更新mMap,相当于更新内存缓存中的map。遍历结束,把放修改内容的mModified map清空,并将这些值调用MemoryCommitResult的构造方法新建一个对象返回。

回过头再来看apply方法,现在相当于已经把内存缓存中的map已经更新到了最新的值了,接下去就是开启一个异步线程去讲修改后的map值回写到磁盘中,我们来看enqueueDiskWrite方法:

在这里插入图片描述
这里关键的写入磁盘方法就是writeToFile方法,我们跟进去看看异步写入磁盘的这个关键方法:

在这里插入图片描述
首先先做了一些优化判断,如果xml文件并且映射关系没有任何变化的话,则直接return了,省的再次写入浪费时间。然后就是对灾备文件的处理,如果存在老的灾备文件的话,就直接删除老的配置文件,相当于做了灾备处理。
然后就是把之前的map配置一次性全部写入mFile中,如果写入成功的话那么久删除灾备文件。
再回到apply方法,因为是异步去做写入磁盘的操作,所以apply中的notifyListeners的通知方法是在内存更新完了以后就直接去通知了,这就是apply和commit方法最大的区别,那么接下来我们就来看看commit方法:

在这里插入图片描述
同样的写入操作,不过这里是同步操作,也就是说commit方法只有在回写磁盘操作全部完成以后才会去notify。

这就是基本上教程中都推荐开发者使用apply方法的原因。

app卡顿

上面在分析apply方法的时候我们可以看到,异步任务去写入磁盘的时候有一个把awaitCommit的runnable放入到QueuedWork的操作,这里主要是为了保证写入到内容不会丢失,才会将每个apply都await存起来,然后在这个队列里依次调用。但是问题也就是在这里,我们来看主线程ActivityThread.java的代码:
在这里插入图片描述
这里我们看到这里会调用QueuedWork.waitToFinish()方法去确保所有挂起的写入操作都执行完成,这个时候会堵塞住主线程。在ActivityThread中我们可以看到在handlePauseActivity,handleStopActivity,handleSleeping,handleServiceArgs和handleStopService5处地方都有调用这个waitToFinish方法。
这里我的理解是因为所有的异步写入任务都是放在一个队列中依次执行,所以会存在在前面的等待任务误被当作free任务而把后面的任务冲掉的可能性,所以Android源码在几个pause,stop的关要处用堵塞主线程的形式去让异步的写入任务都完成,以确保都执行完成。

总结

所以总结如下:

  1. sp是线程安全的,但不是进程安全的,因为他的所有操作基本都加锁了,但没有使用跨进程的锁。
  2. 加载速度比较慢。sp文件的加载时开启异步线程完成的,如果还没加载完成的话读操作就要等加载完成以后才能操作。
  3. 无论是commit还是apply,如果你修改了一个key-value,那么就得全量写入文件。
  4. apply会造成ActivityThread卡顿。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值