Google官方推荐ListView的优化方式是convertView与ViewHolder的结合使用。这样缓存的方式,确实可以很大程度上压缩getView的生成时间,提高ListView的绘制效率,尤其是在快速滚动时。
在我的项目中,ListView的item布局比较复杂,而且列表很长,用户可能会频繁滚动,使用convertView与ViewHolder来优化是理所当然的。使用之后,刚开始确实在用户体验上有一定提升,但是最后却带了一个非常严重的问题——OOM。
在2.X的机器上反反复复创建和销毁Activity,内存一直往上快速增长,最后OOM,很明显是内存泄漏导致的,但是奇怪的是在4.X的机器上,却没有此问题。
与系统版本相关的问题,向来是比较头疼。如果是context, listener以及handler这些容易发生内存泄漏的地方出了问题,肯定是所有的系统都会内存泄漏。用MAT查了一遍,只能看到Activity有多个实例存在,没有找到泄漏的根源。想想发生内存泄漏的本质,是销毁对象时外部还有对象持有该对象的引用。
把Activity的代码又Review了几遍,真的没发现什么可疑的地方。苦思冥想无果,那我就就胡思乱想好了,连那些不太可能出问题的地方也查一遍。在检查ViewHolder的实现方式时,发觉有一个地方与常用的方式不一样。在我的项目中,由于getView外面还封装了一层,其默认的tag已经被占用,要把ViewHolder添加到View中,只能使用setTag的另外一个使用键值对的版本。
思前想后,还是没有什么头绪。最后只能去怀疑那些不太可能出问题的地方了。在检查到ViewHolder的使用时,发现了一个不同寻常的地方,使用了一个带id参数的setTag版本。当初写这样写的原因,是因为默认的Tag已经被其他地方占用了,这里只能使用一个键值对形式的Tag。会不会是这里的原因,先看看源码再说。
先看了一下2.2的系统View.java源码,很快就隐隐约约发现了问题的所在。默认的tag在View中是一个私有的成员变量;而键值对形式的Tag,是先存到一个SparseArray中,然后添加到一个以view为键WeakHashMap中,既然拿view来做键,这个WeakHashMap就肯定是静态的。虽然使用了WeakHashMap,寄希望与靠系统来做回收,但是那向来是不可靠的,即使我手动GC,都不会释放。为了确认是这个地方的问题,我查了2.3,4.0,4.1,4.2的源码,发现2.X的处理方式一致,而4.X中tag都是直接插入到一个私有的SparseArray中。
Google官方说使用ViewHolder可以优化ListView的性能,确实没错,错就错在我的使用方式上。确认了问题所在,只能忍痛去掉了ViewHolder。去掉后,内存泄漏的问题就没有了。至于性能上,损失是必然的,只能先解决燃眉之急,性能的问题等以后再优化。
最保险的做法是,不要使用带键值对版本的tag。ViewHolder还是还标准的方式来使用。