上周在数据平台上发现某个发布版本的OOM数量有明显上升,像往常一样,这种无人认领的野锅还是交到了凤梨的手里。在用leakcanary等工具仔仔细细的扫了两天之后,才发现这个OOM居然不是Context泄漏或者XXXManager无脑缓存这样的一般货色,而是由SharedPreferences(以下简称SP,就是这么懒)的不恰当使用造成的,着实给我单调无聊的刷锅生活带来了一丝惊喜。那么下面就来讲解一下这个问题究竟是如何产生的。
SP是Android中非常常用的轻量级持久存储方式,基本上每个安卓程序员都使用过它存储一些应用状态或者用户标志什么的。这次的问题就出在“轻量级”这三个字上,实际上,一段这样的代码就可以轻松的耗光进程内存:
private void saveBeanUpdateTime(List<Bean> beanList, long time) {
SharedPreferences sp = getSharedPreferences("bean_update_time", MODE_PRIVATE);
for (Bean bean : beanList) {
sp.edit().putLong(bean.getId(), time).apply();
}
}
相信各位老鸟一样就能看出,每个bean的写操作都单独执行了edit()和apply(),非常的不妥。edit()
返回一个Editor对象,实际就是创建了一个编辑事务,而apply()则是将编辑的内容提交保存,Google文档中也要求在SP写操作时要集中提交,不要反复创建Editor,但是在通常的概念里,这样做似乎也只是效率比较差,也没有严重到会导致OOM的程度。那么让我们逐本溯源,看一看SP的内部实现是怎么做的。
SP的实现类是SharedPreferencesImpl,依据SP创建的Context和name,对应的持久存储实际上是app私有目录share_prefs目录下的一个xml文件。而在内存中,SP的主要数据结构是一个Map对象
private Map<String, Object> mMap;
其中存放了SP从对应的xml文件中读出的键值对,实现类为HashMap。所以我们可以认为的一个SP对象占用内存大小和条目数量成正比,还是很紧凑的。
而在坐写操作的时候就没有这么紧凑了:
public Editor edit() {
...
return new EditorImpl();
}
每次执行edit的时候实际都会生成一个崭新的EditorImpl对象。EditorImpl对象中也有一个Map,主要是提交修改的记录,实际并不怎么占用内存,然而在执行apply()的过程中,麻烦来了。
SP写操作提交的过程分为两步,1,在内存中将EditorImpl中记录的修改和SharedPreferencesImpl中的原数据进行合并;2,将合并之后的数据写入xml文件。
在第一步中如果同时存在多个apply请求,每个都会把SP中的Map内存拷贝一份作为内存修改的基准:
if (mDiskWritesInFlight > 0) {
...
mMap = new HashMap<String, Object>(mMap);
}
同时由于执行apply()时,第二步是在一个单线程线程池中执行的,因此第一步申请的内存实际上都被执行队列同时引用着。
由此我们可以知道,当反复执行SP的edit()和apply()操作时,假设操作的次数为M,而SP中的条目数量为N,HashMap.Entry对象最小占用24bytes,那么占用的总内存约为:
MN24*2(HashMap的容积率4/3到8/3之间,取平均数),举个例子,若是M=N=1024,则执行开头那个for循环时申请的内存为48M,这个大小已经足够一些app产生OOM了。
经过这些分析,我们会有以下结论:
1,如果只创建一个Editor并只进行一次apply(),不会有OOM。
2,如果执行的不是apply()而是commit(),也不会有OOM,当然这样的性能会非常感人。
3,循环执行edit()和apply(),达到1k数量级时,很容易引发OOM。
绝大部分(几乎全部)工程中,同学们都喜欢写个SPUtils/SPFile什么的封装一下SP:
public static void saveString(String key, String value) {
sp.edit().putString(key, value).apply();
}
你们要当心了。
最后我们来看一下ContextImpl中对SP的管理代码:
public SharedPreferences getSharedPreferences(String name, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
if (sSharedPrefs == null) {
sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
}
final String packageName = getPackageName();
ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();
sSharedPrefs.put(packageName, packagePrefs);
}
...
sp = packagePrefs.get(name);
if (sp == null) {
File prefsFile = getSharedPrefsFile(name);
sp = new SharedPreferencesImpl(prefsFile, mode);
packagePrefs.put(name, sp);
return sp;
}
}
...
return sp;
}
好消息是ContextImpl对SP做了全局缓存,因此即使你反复调用getSharedPreferences(),并不会创建重复的SharedPreferencesImpl对象。坏消息是这个缓存并没有任何的trim机制,如果你使用的SP足够多而且其中堆放的数据足够多,你还是可能会遇上内存问题:)。
所以还是要时刻谨记,SharedPreferences是一个“轻量级”选手。
网友评论