android内存泄漏原因分析

前言

诸如Java这样的GC(垃圾回收)语言的一个好处就是免去了开发者管理内存分配的必要。这样降低了段错误导致应用奔溃或者未释放的内存挤爆了堆的可能性,因此也能编写更安全的代码。不幸的是,Java里仍有一些其他的方式会导致内存“合理”地泄漏。最终,这意味着android应用可能会浪费一些非必要内存,甚至出现OOM错误。

传统的内存泄漏发生的时机是:所有的相关引用已经不在域范围内,忘记释放内存了。另一方面,逻辑内存的泄漏,是忘记去释放在应用中不再使用的对象引用的结果。如果对象仍然存在强引用,GC就无法从内存中回收对象。这在android开发中尤其是个大问题:如果泄漏了context,(而像activity一样的context持有大量的内存引用,例如,view层级和其他资源)这就意味着泄漏了它引用的东西;android应用通常运行在内存受限的收集设备中,如果应用泄漏了太多内存就会导致OOM错误。

在android中,所有可能导致内存泄漏的陷阱都围绕着两个基本场景:

  • 第一个是由独立于应用状态存在的全局静态对象对activity的链式引用造成的
  • 另外一个是由独立于activity生命周期的一个线程持有activity的应用链造成的

内存泄漏原因

1、静态变量

泄漏一个activity最简单的方法是:定义activity时在内部定义一个静态变量,并将其值设置为处于运行状态的activity。如果activity生命周期结束时没有清除引用的话,这个activity就会泄漏。这是因为这个对象表示这个activity类是静态的并且在内存中一直保持加载状态。如果这个类对象持有了对activity实例的引用,就不会被选中进行GC了。

2、静态View

在activity中,一个被附加的View会持有对它的context的引用。通过使用一个View的静态引用,就相当于给activity设定了一个持久化大的引用链并且泄漏了它。所以不要使附加的View静态化,如果必须这么做的话,至少让它们在activity完成之前从View层级关系的同一点上分离出来。

3、内部类

内部类一般可以提高可靠性和封装性等,但若是创建了一个内部类的实例然后对其持有一个静态引用,就会发生内存泄漏。这是因为内部类的一个特性是它们可以访问外部类的变量,所以它们必然持有了对外部类实例的引用以至于当activity作为外部类的时候就会发生activity的内存泄漏。

4、匿名类

同样地,匿名类持有了外部定义的类的引用。这时候如果在activity中匿名声明并实例化了一个线程任务并让其运行的话就会发生内存泄漏,因为在activity销毁后它仍然在后台工作的话,对于activity的引用会持续并且直到后台工作完成才GC。

5、Handler

相同的情况同样适用于这样的后台任务:被一个Runnable对象定义并被Handler对象加入执行队列。这个Runnable对象将会隐式地引用定义它的activity然后会作为message提交到Handler的MessageQueue(消息队列)。只要activity销毁前消息还没有被处理,那么引用链就会使activity保留在内存里并导致泄漏。

6、监听器

早期Java编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有去删除这些监听器,从而增加了内存泄漏的机会。

7、资源未关闭

对于使用了BroadcastReceiver、ContentObserver、File、游标Cursor、Stream、Bitmap等资源的使用,应该在activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

小结

对activity等组件的引用应该控制在activity的生命周期之内,如果不能就考虑使用getApplicationContext或getApplication,以避免activity被外部长生命周期的对象引用而泄漏。尽量不要在静态变量或者静态内部类中使用非静态内部类中使用外部成员变量,即使要使用,也要考虑适时把外部成员变量置空,也可以在内部类中使用弱引用来引用外部类的变量。对于生命周期比activity长的内部类对象,并且内部类使用了外部类的成员变量,可以将内部类改为静态内部类,静态内部类中使用弱引用来引用外部类的成员变量。