Android 内存泄漏 的解决方案
Android 内存泄漏是很有必要引起极大重视的一个话题,因为绝大部分的内存泄漏是可以避免的,如果开发者的目标只是开发出来,而不思考自己写得代码是否隐藏着风险,是否可以进一步优化,那么毫无疑问,永远无法成为高级工程师。下面我来梳理一下可能引起内存泄漏的代码以及相应的解决方案。
题外话
先穿插一句,我所在的公司 途虎养车网 一直在与 阿里巴巴 合作进行车载系统的开发,我们的 途虎养车 需要集成到 搭载 阿里云系统( YunOS)系统里面去,车载系统上对 app 的性能要求是极其的高。阿里那边的测试妹子也是相当厉害,对内存,CPU资源占用 这一块一直严控。我就结合着自己的项目的内存泄漏点来理一理。
注册的监听器
一般的 view.setOnClickListener(this) 并不会造成内存泄漏,因为 view 与 Activity 紧紧绑在一起,当 Activity 销毁的时候,这些 listener 会成为 garbage,垃圾回收器会随时回收它们。但是有一种情况,比如使用了单例模式来提供注册监听器,由于单例创建后一直存在在系统中,如果没有解注册监听器,那么会一直持有引用,从而导致内存泄漏。系统的 LocationManager 也是一个道理,看下面一段代码。
1 | public class LeaksActivity extends AppCompatActivity implements LocationListener { |
上面用了 Android 系统的位置更新服务,让 Activity 实现了 LocationListener 接口,并重写了它的回调方法。这样系统持有的 LocationListener 就持有了 Activity 的引用。当 Activity 可以旋转 或者 频繁的进入关闭该 Activity 的时候,由于 LocationListener 持有 引用,所以 垃圾回收器并不会回收该 Activity,从而导致了 内存泄漏。重复操作,最终将内存溢出。解决的办法就是在 onDestroy() 方法里移除更新。
1 |
|
另外 动态注册 Broadcastreceiver 与此同理,不要忘了 unregister 。
内部类
内部类用起来太方便了,代码可读性也好,但是使用的时候务必谨慎一下,之所以用起来舒服,是因为内部类隐式地持有了外部类的引用,因此可以访问外部类的成员和方法,但是当内部类是一个 Thread 或者 AnsynTask 的时候,由于 Thread 执行任务 结束的时间是不确定的,当任务没有执行完时,线程是不会被销毁的,因此 它隐式引用的 Activity 也不会销毁。如果是 AnsyncTask 的情况更加糟糕,它的内部 维护了一个 ThreadPoolExecutor,该类创建的线程 的 生命周期是不确定的,或者说是无法控制的,因此更加容易出现内存泄漏。看下面一段代码。
1 | public class AsyncActivity extends AppCompatActivity{ |
上面这段代码,如果是一个 HTTP 请求的话,假设网络不是很好(这个概率是很大的),由于 AsyncTask 又没有取消,哈哈,完蛋了!
如何解决呢?
- 将内部类声明成静态 static 的,削除对外部类的引用。那如何获取 textView 呢?
- 在静态内部类内部保存一个 Context 的引用。思考:是否有问题?static –> Context ?
- 用 弱引用 的形式 保存 Context 的引用。
- onDestroy() 时 取消 AsyncTask。 参见 AsyncTask Document
修改后代码如下:匿名类和内部类一样,同样持有外部类的引用,解决方法和上面相同。我们在 用 Handler 的时候,声明在内部时经常会看到编辑器会有警告 “handler should be static, else it is prone to memory leaks. ” 与上面同理。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
40public class AsyncActivity extends AppCompatActivity{
private TextView textView;
private AsyncTask<Void, Void, String> task;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_async);
textView = (TextView) findViewById(R.id.text_view);
task = new BackgroundTask(this).execute();
}
private static class BackgroundTask extends AsyncTask<Void,Void,String>{
private WeakReference<Context> mContext;
BackgroundTask(Context context){
this.mContext = new WeakReference<>(context);
}
protected String doInBackground(Void... voids) {
return "some str";
}
protected void onPostExecute(String s) {
super.onPostExecute(s);
AsyncActivity activity = (AsyncActivity) mContext.get();
if(activity != null){
activity.textView.setText(s);
}
}
}
protected void onDestroy() {
task.cancel(true);
super.onDestroy();
}
}
资源未释放
代码里长期保持着资源的引用,比如 Context,IO 流,Cursor,Bitmap 等。资源得不到释放,从而导致泄漏。看下面 Android 官方文档的一段关于 context 的长期引用例子。
1 | private static Drawable sBackground; |
sBackground 与 TextView 关联起来了,而且 sBackground 是一个 静态 的成员变量,所以即使 activity 销毁,sBackground 仍然持有 TextView 的引用,TextView 是包含 Context 引用的,所以 最终 activity 并没有释放。
解决方案:
- 尽量避免 static 关键字 引用资源消费过多的实例。
- 尽量使用 ApplicationContext,它的生命周期长,不会出现泄漏的情况。
再举一个 Cursor 不关的例子。
我们的项目有一个单独地跑在后端的 service 进程,每 30s 去获取 阿里提供的天气数据,从而刷新洗车指数等。其中 读取天气指数 是通过读取阿里提供的本地数据库数据实现,所以用到了 Cursor,而且一直没关(不知道谁写的了,找出来绝对要面批),阿里测试反馈了这个bug,我跟进代码才揪出来,游标没关,丢人。
参考资料
Android Performance Patterns on YouTube
Memory leaks in Android — identify, treat and avoid
Memory leakage in event listener
Android : References to a Context and memory leaks
补充
第三方库 检测内存泄漏
LeakCanary 内存泄漏检测的一个库使用 工具 检测内存泄漏
基于Android Studio的内存泄漏检测与解决全攻略
Android Activity泄漏问题解决方案
Android性能优化之内存泄漏
内存分析工具 MAT 的使用
Title: Android 内存泄漏 的解决方案
Author: mjd507
Date: 2016-12-04
Last Update: 2024-01-27
Blog Link: https://mjd507.github.io/2016/12/04/Android-memory-leak/
Copyright Declaration: This station is mainly used to sort out incomprehensible knowledge. I have not fully mastered most of the content. Please refer carefully.