对于一个 Android 开发者,内存泄漏是一个绕不过去的坎儿。尤其是对于大型应用有着复杂的逻辑和变态需求,内存泄漏真的是非常容易出现,因此打算写两篇文章来对于内存泄漏来进行下分析讨论,尽量避免泄漏的发生。

内存泄漏

说到内存泄漏不得不提下 C/C++,在 C/C++ 中,new/malloc 一个对象会在堆内存生成一个实例对象,如果不进行 delete/free,对象则会一直存在,如果你丢到了指针则意味着你失去了对实例的控制,那么这个实例在内存中存在不会被释放掉,内存泄漏就会发生,因此 C++ 提出了基于引用计数法的智能指针来管理对象实例。Java 语言不同于 C++,有着自己的GC机制可以“自动的”释放“不再实用的实例”内存。既然如此,Java 也会有内存泄漏出现么?答案是肯定的,在进一步解释原因之前,我们先来了解下 Java 的4种引用类型。

引用类型

java对象的引用包括
强引用,软引用,弱引用,虚引用

强引用(StrongReference)

是指创建一个对象并把这个对象赋给一个引用变量。
比如:

强引用有引用变量指向时永远不会被垃圾回收,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。Vector类的clear方法中就是通过将引用赋值为null来实现清理工作的。

软引用(SoftReference)

如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它;
如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

SoftReference 的特点是它的一个实例保存对一个 Java 对象的软引用, 该软引用的存在不妨碍垃圾收集线程对该 Java 对象的回收。
也就是说,一旦 SoftReference 保存了对一个 Java 对象的软引用后,在垃圾线程对 这个 Java 对象回收前,SoftReference 类所提供的get ()方法返回
Java 对象的强引用。 一旦垃圾线程回收该 Java 对象之 后,get() 方法将返回 null。

垃圾收集线程会在虚拟机抛出 OutOfMemoryError 之前回收软可及对象,而且虚拟机会尽可能优先回收长时间闲置不用的软可及对象,对那些刚刚构建的或刚刚使用过的“新”软可反对象会被虚拟机尽可能保留

弱引用(WeakReference)

弱引用也是用来描述非必需对象的,当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。
不过要注意的是,这里所说的被弱引用关联的对象是指只有弱引用与之关联,如果存在强引用同时与之关联,则进行垃圾回收时也不会回收该对象(软引用也是如此)。

虚引用(PhantomReference)

虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。 如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。

因此可以看到,我们在代码中最常使用就是强引用了,同时也是代码中会出现内存泄漏的源头了。

泄漏原因

知道了四种引用类型,我们来看下泄漏的最大原因:

由于对持有 Context 强引用造成泄漏

这种泄漏是最常见的泄漏也是最容易排查的泄漏,具体体现在持有 Context
变量和对于生命周期外的 context引用。
我们先看第一种情况:

持有 static Context 变量

例如 static View, static Activity 这些都是持有了静态的 Context 引用。

我们知道对于静态变量,是不会随着实例对象的释放而消失,而是贯穿这个应用的生命周期的,所以被泄漏的 Activity 就会一直存在于应用的进程中,不会被垃圾回收器回收。同样这个 TextView 也是一样的道理。而我们知道 Activity 继承 Context,View 包含 Context,所以从本质属于 Context 变量。

同样还有一种内部类,匿名内部类造成的泄漏。我们都知道内部类默认“持有外部对象”的引用。因此定义为非 static 的内部类,和匿名内部类就相当于对于外部对象的强引用的持有,加入外部类是个 Activity 那就可能会发生向上面说的那种情况。

我们可以看到,一个 static 对象 inner 持有了这个 Activity 的强引用,因此会造成内存泄漏。
接下来是第二种情况:

生命周期外的 Context 引用

这种比较常见的是 Handler 与 Thread 的使用:

这个 Handler 和 Thread 虽然也持有整个 Activity 的引用,不过当二者没执行任何操作的时候,Activity 结束了,是不会有问题的。但是当这个 Thread正在 run,或者这 Handler 的消息队列 MessageQueue 中有未处理完的Message,这个 Handler 就无法释放,Activity 实例不会被销毁了,于是导致内存泄漏。

因此造成内存泄漏的第一个原因就是这些,对于 Context 对象的长时间持有,和未再 Activty 生命周期内释放引用。

其他常见的泄漏原因

最常见的 unregisterReveiver(BroadcastReceiver) 的使用,还有对于
Bitmap 资源问题,Cursor 的关闭问题,Animation 的停止问题,这些方面都可能导致内存泄漏

避免内存泄漏

上面说的这么多,其本质都是由于持有引用 Context 而未释放而造成的问题,因此我们可以使用一下办法来避免内存泄漏:

手动控制 static Context 的“生命周期”

这里使用“”是因为我们并不是真正的控制其变量的生命周期,而是在不使用对象的时候对其手动置空。

这种办法可以很有效的解决由于持有 static Context 对象而造成的内存泄漏,不过依然不建议对于 View 持有静态对象。

使用全局的 Contex t对象

这种方式常出现在于单例模式里,例如我要编写一个 Manager,是一个单例,而在创建的时候,又需要持有一个 static Context,因此会在创建的时候使用
Application 对象来进行,这也是为什么大多数第三方库在初始化时要求在
Application 的 onCreate 方法中进行。

使用弱引用

那如果需要的地方并不是一个单例,没必要长时间持有 Context 对象,那么可以用弱引用来保存 Context 对象。

弱引用不会阻止对象的内存释放,所以即使有弱引用的存在,该对象也可以被回收。

避免使用内部类和匿名内部类,多用静态内部类

前面我们看到的都是持有全局生命周期的静态成员变量引起的,直接或间接通过链式引用 Activity 导致的泄漏。只要不跨越生命周期,内部类是完全没问题的。但是,这些类是用于产生后台线程的,这些 Java 线程是全局的,而且持有创建者的引用(即匿名类的引用),而匿名类又持有外部类的引用。线程是可能长时间运行的,所以一直持有 Activity 的引用导致当销毁时无法回收。

对于使用静态内部类和,它并不持有外部类的引用,但是在逻辑上有和内部类一样和外部类保持逻辑关系。可以较好的解决内存泄漏的问题。

Tagged:

发表评论

电子邮件地址不会被公开。 必填项已用*标注