Android 内存泄漏

内存泄漏是一个隐形炸弹,其本身并不会造成程序异常,但是随着量的增长会导致其他各种并发症:OOM,UI 卡顿等。
简介
每个应用程序都需要内存来完成工作,为了确保 Android 系统的每个应用都有足够的内存,Android 系统需要有效地管理内存分配。当内存不足时,Android 运行时就会触发 GC,GC 采用的垃圾标记算法为可达性分析算法。
当应用内部不再需要某个实例后,但是这个对象却仍然被引用,这个无用的对象持续占有我们的内存,此对象到 GC Roots 是可达的(对象被引用),导致无法被 GC 回收,这个情况就叫做内存泄露 (Memory Leak)。安卓虚拟机为每一个应用分配一定的内存空间,当内存泄露到达一定的程度就会造成内存溢出。
内存泄漏产生的原因,主要分为三大类:
- 由开发人员自己编码造成的泄漏
- 第三方框架造成的泄漏
- 由 Android 系统或者第三方 ROM 造成的泄漏
通常情况下,第二种和第三种情况对于 Android 应用开发者来说是不可控的,所以主要避免可控的内存泄漏。
内存泄漏优化:分为两方面
- 开发过程中避免写出有内存泄漏的代码
- 通过一些分析工具比如 MAT 来找出潜在的内存泄露继而解决
出现内存泄露的根本性的原因,就是长生命周期的对象持有短生命周期对象的引用,从而导致短生命周期对象无法及时被 GC 回收。而静态类,常量这种的生命周期是和程序一致的,如果被他们持有了引用,则一直是可达状态,就无法被 GC 回收。
常见内存泄漏的场景
1、非静态内部类的静态实例
非静态内部类会持有外部类实例的引用,如果非静态内部类的实例是静态的,就会间接地长期维持着对外部类的引用,阻止被系统回收。
1 | public class TestActivity extends AppCompatActivity { |
2、多线程相关的匿名内部类 / 非静态内部类
匿名内部类也会持有外部类实例的引用。多线程相关的类有 AsyncTask 类、Thread 类和实现 Runnable 接口的类等,它们的匿名内部类 / 非静态内部类如果做耗时操作就可能发生内存泄漏,因为线程和 Activity 的生命周期是不一致的,下面以 AsyncTask 的匿名内部类举例。
1 | public class TestActivity extends AppCompatActivity { |
解决的办法就是自定义一个静态的 AsyncTask。这样它的生命周期与外部的 Actiity 就无关了。
1 | public class TestActivity extends AppCompatActivity { |
非静态内部类与匿名内部类是如何持有外部类的引用的:
类上右键 -》 选择 Open in Terminal -》键入 javac Test.java -》 会生成相应的 class 文件(包括内部类的 class 文件)
内部类虽然和外部类写在同一个文件中, 但是编译完成后, 还是生成各自的 class 文件,内部类通过 this 访问外部类的成员。
- 编译器自动为内部类添加一个成员变量, 这个成员变量的类型和外部类的类型相同, 这个成员变量就是指向外部类对象 (this) 的引用;
- 编译器自动为内部类的构造方法添加一个参数, 参数的类型是外部类的类型, 在构造方法内部使用这个参数为内部类中添加的成员变量赋值;
- 在调用内部类的构造函数初始化内部类对象时,会默认传入外部类的引用。
示例:
1 | public class Test { |
通过 javac 编译,可以得到 class 文件(包括内部类的 class 文件)
内部类的 class 文件内容如下:(Test$InnerClass.class)
1 | public class Test$InnerClass { |
3、Handler 内存泄漏
首先要说明一点,Handler 属于 TLS(Thread Local Storage)变量,生命周期和 Acyivity 是不一致的。
每当创建一个 Handler 时,都会自动与当前线程的 Looper 绑定,并且 Message 会持有对此 Handler 的引用。这时,如果有消息未处理,因为 Message 持有对 Handler 的引用,非静态的 Handler 又会潜在的持有对外部类的引用,就会导致类无法被回收。关键点,就是这个 Handler 是非静态的。
Handler 通过发送 Message 与主线程交互,Message 发出之后是存储在 MessageQueue 中的,有些 Message 也不是马上就被处理的。在 Message 中存在一个 target,是 Handler 的一个引用,如果 Message 在 Queue 中存在的时间越长,就会导致 Handler 无法被回收。如果 Handler 是非静态的,则会导致 Activity 或者 Service 不会被回收。由于 AsyncTask 内部也是 Handler 机制,同样存在内存泄漏的风险。
此种内存泄露,一般是临时性的。
1 | public class TestActivity extends AppCompatActivity { |
解决方案有两个:
使用一个静态的 Handler 内部类,Handler 持有的对象要使用弱引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43public class TestActivity extends AppCompatActivity {
private Button btn;
private MyHandler myHandler = new MyHandler(this);
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
btn = findViewById(R.id.btn);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
myHandler.sendMessageDelayed(Message.obtain(),60000);
finish();
}
});
}
public void show(){
}
/**
* 静态内部类
* 持有的 Activity 对象使用了弱引用,这样就避免了内存泄漏。
*/
private static class MyHandler extends Handler{
private final WeakReference<TestActivity> mActivity;
public MyHandler(TestActivity testActivity){
mActivity = new WeakReference<>(testActivity);
}
@Override
public void handleMessage(@NonNull Message msg) {
if (mActivity !=null && mActivity.get() == null){
mActivity.get().show();
}
}
}
}在 Activity 的 onDestroy () 中移除 MessageQueue 中的消息。
1
2
3
4
5
6
7@Override
protected void onDestroy() {
if (myHandler != null){
myHandler.removeCallbacksAndMessages(null);
}
super.onDestroy();
}
4、未正确使用 Context
对于不是必须使用 Activity 的 Context 的情况(Dialog 的 Context 必须使用 Activity 的 Context),可以考虑使用 Application Context 来代替,这样可以避免 Activity 泄漏,比如如下的单例模式:
单例对象在初始化后将在 JVM 的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被 JVM 正常回收,导致内存泄漏。。比如,当需要 Context 时,请使用 Application 的 Context。
1 | public class TestSingleton { |
解决办法,使用 Application 的 Context。
1 | public final void setup(Context mContext){ |
5、静态 View
使用静态 View 可以避免每次启动 Activity 都去读取并渲染 View,但是静态 View 会持有 Activity 的引用,导致 Activity 无法被回收,解决的办法就是在 onDestroy () 中将静态 View 设置为 null。
1 | public class TestActivity extends AppCompatActivity { |
6、WebView
不同的 Android 版本的 WebView 会有差异,加上不同厂商定制 ROM 的 WebView 的差异,这就导致 WebView 存在着很大的兼容性问题。WebView 都会存在内存泄漏的问题,在应用中只要使用一次,内存就不会被释放掉。通常的解决办法就是为 WebView 单开一个进程,使用 AIDL 与应用的主进程进行通信。WebView 进程可以根据业务需求,在合适的时机进行销毁。
7、资源对象未关闭
各种连接,比如数据库连接,网络连接(socket),IO 连接。资源对象未关闭,资源性对象如 Cursor、File、Socket,应该在使用后及时关闭。未在 finally 中关闭,会导致异常情况下资源对象未被释放的隐患。
BraodcastReceiver,Bitmap 等资源,应该在 Activity 销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。注册对象未反注册。未反注册会导致观察者列表里维持着对象的引用,阻止垃圾回收。
资源对象如 Cursor、File 等,往往都使用了缓冲,会造成内存泄漏。因此,在资源对象不使用时,一定要确保它们已经关闭并将它们的引用设置为 null,通常在 finally 语句中进行关闭,防止出现异常时,资源未被释放的问题。
8、集合中对象没清理
通常把一些对象的引用加入到了集合中,当不需要该对象时,如果没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是 static 的话,那情况就会更加严重。有时,可能光是给对象置空是不够的( == null ),还要记得将集合清空( .clear () ),因为集合还是持有对象的引用。(还有一点,据说当集合里面的对象属性被修改后,再调用 remove () 方法时不起作用。)
9、Bitmap 对象
临时创建的某个相对比较大的 Bitmap 对象,在经过变换得到新的 Bitmap 对象之后,应该尽快回收原始的 Bitmap,这样就能够更快释放原始 Bitmap 所占用的空间。避免静态变量持有比较大的 Bitmap 对象或者其他大的数据对象,如果已经持有,要尽快置空该静态对象。
10、监听器未关闭
很多系统服务(比如 TelephonyMannager、SensorManager)需要 register 和 unregister 监听器,需要确保在合适的时候及时 unregister 那么监听器。自己手动添加的 Listener,要记得在合适的时候及时移除这个 Listener。
11、外部模块的引用:
当调用其他模块的方法时,并且传入了一个对象作为参数。这时,调用的这个模块就有可能会一直保持持有这个对象参数的引用。
在项目中经常会使用各种三方库,有些三方库的初始化需要我们传入一个 Context 对象。但是三方库中很有可能一直持有此 Context 引用。
12、其它:不良代码,静态变量导致的内存泄漏,单例模式导致的内存泄露,属性动画导致的内存泄露。
分析工具
查找内存泄露;
Android Studio 自带的 Android Profiler 工具。通过 Android Studio 窗口进行分析,查看内存分配情况,如果操作应用时内存一直往上涨说明存在内存泄露。
使用开源库 LeadCanary(Square 产品)快速定位内存泄露。
Memory Monitor
Memory Profiler 是 Android Profiler 中的一个组件,Android Profiler 是 Android Studio 3.0 用来替换之前 Android Monitor 的观察工具,主要用来观察内存,网络,cpu 温度。Profiler 在 Android Studio 的底部。
大内存申请与 GC:
当分配的内存急剧上升,这就是大内存分配的场景,我们要判断这是否是合理的分配的内存,是 Bitmap 还是其他的大数据,并且对这种大数据进行优化,减少内存开销。当分配的内存出现急剧下降,这表示垃圾收集事件,用来释放内存。
内存抖动:
内存抖动一般指在很短的时间内发生了多次内存分配和释放,严重的内存抖动还会导致应用程序卡顿。内存抖动出现的原因主要是短时间频繁地创建对象(可能在循环中创建对象),内存为了应对这种情况,也会频繁地进行 GC。非并行 GC 在进行时,其它线程都会被挂起,等待 GC 操作完成后恢复工作。如果是频繁的 GC 就会产生大量的暂停时间,这会导致界面绘制时间减少,从而使得多次绘制一帧的时长超过了 16ms,产生的现象就是界面卡顿。综合起来就产生了内存抖动,产生了锯齿状的抖动图示。
Allocation Tracker
用来跟踪内存分配,它允许在执行某些操作的同时监视在何处分配对象,了解这些分配能够调整与这些操作相关的方法调用,以优化应用程序性能和内存使用。
Heap Dump
主要功能就是查看不同的数据类型在内存中的使用情况。可以帮助我们找到大对象,也可以通过数据的变化发现内存泄漏。
MAT
在进行内存分析时,可以使用 Memory Monitor 和 Heap Dump 观察内存的使用情况,使用 Allocation Tracker 跟踪内存分配的情况,也可以通过这些工具来找到疑似发生内存泄漏的位置。但是如果想要深入地进行分析并确定内存泄漏,就要分析疑似发生内存泄漏时所生成的堆存储文件。堆存储文件可以使用 DDMS 或者 Memory Monitor 来生成,输出的文件格式为 hprof,而 MAT 就是分析堆存储文件的。MAT 全称为 Memory Analysis Tool,是对内存进行详细分析的工具,它是 Eclipse 的插件,如果用 Android Studio 进行开发则需要单独下载它。下载地址为:http://www.eclipse.org/mat/
LeakCanary
如果使用 MAT 来分析内存问题,会有一些难度,并且效率也不是很高,对于一个内存泄漏问题,可能要进行多次排查和对比。为了能够迅速地发现内存泄漏,Square 公司基于 MAT 开源了 LeakCanary。
通过它可以在 App 运行过程中检测内存泄漏,当内存泄漏发生时会生成发生泄漏对象的引用链,并通知程序开发人员。
可以看出 LeakCanary 主要分 2 大核心部分:
如何检测内存泄漏;
分析内存泄漏对象的引用链。
Java 中的 WeakReference 是弱引用类型,每当发生 GC 时,它所持有的对象如果没有被其他强引用所持有,那么它所引用的对象就会被回收。
WeakReference 的构造函数可以传入 ReferenceQueue,当 WeakReference 指向的对象被垃圾回收器回收时,会把 WeakReference 放入 ReferenceQueue 中。
LeakCanary 中对内存泄漏检测的核心原理就是基于 WeakReference 和 ReferenceQueue 实现的。
当一个 Activity 需要被回收时,就将其包装到一个 WeakReference 中,并且在 WeakReference 的构造器中传入自定义的 ReferenceQueue。
然后给包装后的 WeakReference 做一个标记 Key,并且在一个强引用 Set 中添加相应的 Key 记录。
最后主动触发 GC,遍历自定义 ReferenceQueue 中所有的记录,并根据获取的 Reference 对象将 Set 中的记录也删除。
经过上面 3 步之后,还保留在 Set 中的就是:应当被 GC 回收,但是实际还保留在内存中的对象,也就是发生泄漏了的对象。
备注
参考资料:
Android 进阶解密
Android 工程师进阶 34 讲 - 拉钩
预览: