Android App 性能优化(二)----内存泄露(Memory Leak)

App 性能优化系列:
Android App 性能优化(二)—-内存泄露(Memory Leak)
Android App 性能优化(一)—-布局优化

一. 什么是内存泄露

内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

当一个对象使已经用完不需要时,这时候应该被回收才对,但由于另外一个正在使用的对象直接或者间接的持有它的引用从而导致它不能被回收,这时就会导致本应该被系统回收的内存不能被回收而占用着堆内存,内存泄漏就产生了;

二. 内存泄露对程序的影响

造成应用程序OOM的主要原因之一,当应用中多处地方内存泄漏时可能导致内存紧缺,如果当没有内存可分配时就造成内存溢出,最终程序崩溃;

一般情况下程序在运行的过程中会不断地去回收使用完的内存,但由于内存泄漏会导致内存过快的出现紧缺,这时可能会导致系统频繁的GC,从而导致程序越来越卡,最终内存溢出崩溃;

内存溢出
OutOfMemoery,是指APP向系统申请超过最大阀值的内存请求,系统不会再分配多余的空间,就会造成OOM error。

三. Java 的内存管理

要分析内存泄露就必须要知道 Java 内存管理, 要知道对象的分配和回收.
在 Java 中,通过关键字 new 为每个对象申请内存空间,所有的对象都在堆中分配空间.
而内存的释放是由垃圾回收器GC完成的,程序创建的每一个对象垃圾回收器GC都要对它的运行状态进行监控, 包括对象的申请、赋值, 被引用等. 垃圾回收机制确实简化了程序员的工作, 但同时也带来了性能上的影响,这也是Java程序运行速度比较慢的重要原因.

垃圾回收器GC回收一个对象的条件是这个对象不再被引用, 具体怎么做到这点的就要去了解垃圾回收器的工作原理了, 下面简单的介绍下目前常用的垃圾回收器的工作原理:

为了更好理解垃圾回收器GC的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。另外,每个线程对象可以作为一个图的起始顶点,例如大多程序从main进程开始执行,那么该图就是以main进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被GC回收. Java使用有向图的方式进行内存管理,可以消除引用循环的问题.

Java 中的内存分配

  1. 静态储存区:

编译时就分配好,在程序整个运行期间都存在, 存放在对象中用static定义的静态成员;

栈区

(1)在堆中产生了一个对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于对象在堆内存中的首地址,栈中的这个变量就成了对象的引用变量
(2). 在方法中定义的一些基本类型的变量数据和对象的引用变量都在方法的栈内存中分配, 当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当该方法执行完之后,Java会自动释放掉该方法中定义的变量以及对象所分配的内存空间.

堆区

通常存放 new 出来的对象。由 Java 垃圾回收器回收。堆内存用来存放由new创建的对象和数组。 在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。
当然java内存分配还有其他几种, 比如常量池, 寄存器等, 但这些都和垃圾回收无关,暂不讨论.

三. Java中四种引用类型

强引用(StrongReference)

如果一个对象具有强引用,它就不会被垃圾回收器回收, 即使当前内存空间不足,JVM 也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。如果想中断强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样JVM在合适的时间就会回收该对象;

软引用(SoftReference)

在使用软引用时,如果内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器回收,只有在内存不足时,软引用才会被垃圾回收器回收。
软引用基本上和弱引用差不多,只是相比弱引用,它阻止垃圾回收期回收其指向的对象的能力强一些。如果一个对象是弱引用可到达,那么这个对象会被垃圾回收器接下来的回收周期销毁。但是如果是软引用可以到达,那么这个对象只有当内存不足的时候才会被垃圾回收器回收。 软引用可用来实现内存敏感的高速缓存

弱引用(WeakReference)

具有弱引用的对象拥有的生命周期更短暂,因为当 JVM 进行垃圾回收,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。
Java使用WeakReference表示相应的对象为弱引用,垃圾回收器会帮你来决定引用的对象何时回收并且将对象从内存移除。

虚引用(PhantomReference)

如果一个对象仅持有虚引用,在任何时候都可能被垃圾回收,虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列联合使用,虚引用主要用来跟踪对象 被垃圾回收的活动。
与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

四. Android 中常见的内存泄漏

1. 单例造成的内存泄露

单例的静态特性导致其生命周期同应用一样长。
单例模式的静态特性使得单例的生命周期和 Application 的生命周期一样长,这就会导致如果一个对象已经不需要使用了,而单例对象还持有该对象的引用,那么这个对象将不能被正常回收,这就导致了内存泄漏了;

具体来说在Android中创建单例的时候有时需要传入一个Context, 那么此时这个 Activity 就会被这个单例持有,就会造成 Activity 无法正常回收释放。

请不要传入 Activity 的 Context,而应该传入 Application 的 Context,因为 Application 的 Context 生命周期与应用程序一样长,所以就不会造成内存泄漏了。

解决方案:

将该属性的引用方式改为弱引用; 如果传入Context,使用ApplicationContext;

2. 非静态内部类

在Java中,非静态内部类和匿名类都会潜在的引用它们所属的外部类,但是,静态内部类却不会。如果这个非静态内部类实例做了一些耗时的操作,就会造成外围对象不会被回收,从而导致内存泄漏。

非静态内部内为什么会造成内存泄露?

内部类虽然和外部类写在同一个文件中, 但是编译完成后, 还是生成各自的class文件,内部类通过this访问外部类的成员。
1 编译器自动为内部类添加一个成员变量, 这个成员变量的类型和外部类的类型相同, 这个成员变量就是指向外部类对象(this)的引用;
2 编译器自动为内部类的构造方法添加一个参数, 参数的类型是外部类的类型, 在构造方法内部使用这个参数为内部类中添加的成员变量赋值;
3 在调用内部类的构造函数初始化内部类对象时,会默认传入外部类的引用。

解决方案:
将内部类变成静态内部类; 如果有强引用Activity中的属性,则将该属性的引用方式改为弱引用; 在业务允许的情况下,当Activity执行onDestory时,结束这些耗时任务;

3. Context 的不正确使用

在Android应用程序中通常可以使用两种Context对象:Activity和Application。当类或方法需要Context对象的时候常见的做法是使用第一个作为Context参数。这样就意味着View对象对整个Activity保持引用,因此也就保持对Activty的所有的引用。 假设一个场景,当应用程序有个比较大的Bitmap类型的图片,每次旋转是都重新加载图片所用的时间较多。为了提高屏幕旋转是Activity的创建速度,最简单的方法时将这个Bitmap对象使用Static修饰。 当一个Drawable绑定在View上,实际上这个View对象就会成为这份Drawable的一个Callback成员变量。而静态变量的生命周期要长于Activity。导致了当旋转屏幕时,Activity无法被回收,而造成内存泄露。

解决方案:
使用ApplicationContext代替ActivityContext,因为ApplicationContext会随着应用程序的存在而存在,而不依赖于activity的生命周期;对Context的引用不要超过它本身的生命周期,慎重的对Context使用“static”关键字。Context里如果有线程,一定要在onDestroy()里及时停掉。

4. Handler引起的内存泄漏

当Handler中有延迟的的任务或是等待执行的任务队列过长,由于消息持有对Handler的引用,而Handler又持有对其外部类的潜在引用,这条引用关系会一直保持到消息得到处理,而导致了Activity无法被垃圾回收器回收,而导致了内存泄露。

当使用内部类或者匿名类来创建Handler的时候,Handler对象会隐式地持有一个外部类对象(activity)的引用,而Handler通常会伴随着一个耗时的后台线程一起出现,这个后台线程在任务执行完毕之后,通过消息机制通知Handler,然后Handler再更新界面。但是如果此时Activity退出,正常情况下,Activity不再被使用,它就有可能在GC检查时被回收掉,但由于这时线程尚未执行完,而该线程持有Handler的引用,这个Handler又持有Activity的引用,就导致该Activity无法被回收,直到处理耗时任务的线程结束。
如果使用Handler的postDelayed()方法去开启一个后台线程时,在这个延迟任务没有开启之前都会有一条MessageQueue -> Message -> Handler -> Activity的引用链,导致你的Activity被持有引用而无法被回收。

解决办法:
可以把Handler类放在单独的类文件中,或者使用静态内部类便可以避免泄露; 如果想在Handler内部去调用所在的Activity,那么可以在handler内部使用弱引用的方式去指向所在Activity.使用Static + WeakReference的方式来达到断开Handler与Activity之间存在引用关系的目的。
在Activity结束后,关闭线程,如果你的Handler是被delay的Message持有了引用,那么调用removeCallbacks方法来移除消息队列。

5. 注册监听器的泄漏

系统服务可以通过Context.getSystemService 获取,它们负责执行某些后台任务,或者为硬件访问提供接口。如果Context 对象想要在服务内部的事件发生时被通知,那就需要把自己注册到服务的监听器中。然而,这会让服务持有Activity 的引用,如果在Activity onDestory时没有释放掉引用就会内存泄漏。

解决方案:
使用ApplicationContext代替ActivityContext; 在Activity执行onDestory时,调用反注册;

6. Cursor,Stream没有close,View没有recyle

资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于 java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏。因为有些资源性对象,比如SQLiteCursor(在析构函数finalize(),如果我们没有关闭它,它自己会调close()关闭),如果我们没有关闭它,系统在回收它时也会关闭它,但是这样的效率太低了。因此对于资源性对象在不使用的时候,应该调用它的close()函数,将其关闭掉,然后才置为null. 在我们的程序退出时一定要确保我们的资源性对象已经关闭。

解决方案:
调用onRecycled()

7. 引用没释放

1. 注册服务没取消导致内存泄漏
ContentObserver,FileObserver在Activity onDeatory或者某类声明周期结束之后一定要unregister掉,否则这个Activity类会被system强引用,不会被内存回收。

2. 集合中的对象没有及时清理导致的内存泄露
当该集合为静态的时候,那么在集合里面对象越来越多的时候,要及时清理不需要用到的对象。

解决方案:
在Activity退出之前,将集合里的东西clear,然后置为null,再退出程序。

8. WebView造成的泄露

当我们不要使用WebView对象时,应该调用它的destory()函数来销毁它,并释放其占用的内存,否则其占用的内存长期也不能被回收,从而造成内存泄露。

解决方案:
为webView开启另外一个进程,通过AIDL与主线程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。

9. 动画

在属性动画中有一类无限循环动画,如果在Activity中播放这类动画并且在onDestroy中去停止动画,那么这个动画将会一直播放下去,这时候Activity会被View所持有,从而导致Activity无法被释放。解决此类问题则是需要早Activity中onDestroy去去调用objectAnimator.cancel()来停止动画。

10. Activity 泄漏

如果你持有一个未使用的 Activity 的引用,其实也就持有了 Activity 的布局,自然也就包含了所有的 View。最棘手的是持有静态引用。别忘了,Activity 和 Fragment 都有自己的生命周期。一旦我们持有了静态引用,Activity 和 Fragment 就不会被垃圾回收器清理掉了。这就是为什么静态引用很危险。

参考
[1]http://blog.csdn.net/u013495603/article/details/50696170

代码交流 2021