什么是内存泄漏?
简单点说,就是指一个对象不再使用,本应该被回收,但由于某些原因导致对象无法回收,仍然占用着内存,这就是内存泄漏。
为什么会产生内存泄漏,内存泄漏会导致什么问题?
相比C++需要手动去管理对象的创建和回收,Java有着自己的一套垃圾回收机制,它能够自动回收内存,但是它往往会因为某些原因而变得“不靠谱”。
在Android开发中,一些不好的编码习惯就很可能会导致内存泄漏,而这些内存泄漏会导致应用内存越占越大,使得应用变得卡顿,甚至造成OOM(Out Of Memory)内存溢出问题,同时也使应用变得极其不稳定,因为当内存不足的时候,系统会优先回收那些“内存占比”大的应用。
Java的内存分配机制
首先我们先来了解下Java的内存分配机制,Java 程序运行时的内存分配策略有三种,分别是静态分配,栈式分配,和堆式分配,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、栈区和堆区。
静态存储区(方法区):主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。
栈区 :当方法被执行时,方法体内的局部变量(其中包括基础数据类型、对象的引用)都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
堆区 : 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存,也就是对象的实例。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。
那什么样的对象会被回收呢?
Java内存管理有向图
为了更好理解 GC 的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。另外,每个线程对象可以作为一个图的起始顶点,例如大多程序从 main 进程开始执行,那么该图就是以 main 进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被 GC 回收。
常见的内存泄漏和解决方案
1、单例引起的内存泄漏
由于单例的静态特性导致它的生命周期和整个应用的生命周期一样长,如果有对象已经不再使用了,但又却被单例持有引用,那么就会导致这个对象就没办法被回收,从而导致内存泄漏。
// 使用了单例模式
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context;
}
public static AppManager getInstance(Context context) {
if (instance != null) {
instance = new AppManager(context);
}
return instance;
}
}
问题所在:
从上面的代码我们可以看出,在创建单例对象的时候,引入了一个Context上下文对象,如果我们把Activity注入进来,会导致这个Activity一直被单例对象持有引用,当这个Activity销毁的时候,对象也是没有办法被回收的。
解决方案:
在这里我们只需要让这个上下文对象指向应用的上下文即可(this.context=context.getApplicationContext()),因为应用的上下文对象的生命周期和整个应用一样长。
2、非静态内部类创建静态实例引起的内存泄漏
由于非静态内部类会默认持有外部类的引用,如果我们在外部类中去创建这个内部类对象,当频繁打开关闭Activity,会导致重复创建对象,造成资源的浪费,为了避免这个问题我们一般会把这个实例设置为静态,这样虽然解决了重复创建实例,但是会引发出另一个问题,就是静态成员变量它的生命周期是和应用的生命周期一样长的,然而这个静态成员变量又持有该Activity的引用,所以导致这个Activity销毁的时候,对象也是无法被回收的。
public class MainActivity extends AppCompatActivity {
private static TestResource mResource = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(mResource == null){
mResource = new TestResource();
}
//...
}
class TestResource {
//...
}
}
问题所在:
其实这个和上面单例对象的内容泄漏问题是一样的,由于静态对象持有Activity的引用,导致Activity没办法被回收。
解决方案:
在这里我们只需要把非静态内部类改成静态内部类即可(static class TestResource)。
3、Handler引起的内存泄漏
记得我们刚学习Handler的时候,网上资料甚至学校教材“教科书”式的写法都是这样的
Handler mHandler=new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//to do something..
switch (msg.what){
case 0:
//to do something..
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new Thread(new Runnable() {
@Override
public void run() {
//to do something..
mHandler.sendEmptyMessage(0);
}
}).start();
}
问题所在:
别看上面短短几行代码,其实涉及到了很多问题,首先我们知道程序启动时在主线程中会创建一个Looper对象,这个Looper里维护着一个MessageQueue消息队列,这个消息队列里会按时间顺序存放着Message,不清楚的朋友可以看下我之前写的这篇文章《从源码的角度彻底理解Android的消息处理机制》,然后上面的Handler是通过内部类来创建的,内部类会持有外部类的引用,也就是Handler持有Activity的引用,而消息队列中的消息target是指向Handler的,也就等同消息持有Handler的引用,也就是说当消息队列中的消息如果还没有处理完,这些未处理的消息(也可以理解成延迟操作)是持有Activity的引用的,此时如果关闭Activity,是没办法回收的,从而就会导致内存泄露。
解决方案:
和上文一样,我们需要先把非静态内部类改成静态内部类(如果是Runnable类也需要改成静态),然后在Activity的onDestroy中移除对应的消息,再来需要在Handler内部用弱引用持有Activity,因为让内部类不再持有外部类的引用时,程序也就不允许Handler操作Activity对象了。
MyHandler myHandler = new MyHandler(this);
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new Thread(new Runnable() {
@Override
public void run() {
myHandler.sendMessage(Message.obtain());
}
}).start();
}
@Override
protected void onDestroy() {
super.onDestroy();
//移除对应的Runnable或者是Message
//mHandler.removeCallbacks(runnable);
//mHandler.removeMessages(what);
mHandler.removeCallbacksAndMessages(null);
}
private static class MyHandler extends Handler {
private WeakReference mActivity;
public MyHandler(Activity activity) {
mActivity = new WeakReference(activity);
}
@Override
public void handleMessage(Message msg) {
if (mActivity.get() == null) {
return;
}
//to do something..
}
};
4、WebView引起的内存泄露
关于WebView的内存泄漏,这是个绝对的大大大大大坑!不同版本都存在着不同版本的问题,这里我只能给出我平时的处理方法,可能不同机型上存在的差异,只能靠积累了。
方法一:
首先不要在xml去定义,定义一个ViewGroup就行,然后动态在代码中new WebView(Context context)(传入的Context采取弱引用),再通过addView添加到ViewGroup中,最后在页面销毁执行onDestroy()的时候把WebView移除。
方法二:
简单粗暴,直接为WebView新开辟一个进程,在结束操作的时候直接System.exit(0)结束掉进程,这里需要注意进程间的通讯,可以采取Aidl,Messager,Content Provider,Broadcast等方式。
5、Asynctask引起的内存泄露
这部分和Handler比较像,其实也是因为内部类持有外部类引用,一样的改成静态内部类,然后在onDestory方法中取消任务即可。
6、资源对象未关闭引起的内存泄露
这块就比较简单了,比如我们经常使用的广播接收者,数据库的游标,多媒体,文档,套接字等。
7、其他一些
还有一些需要注意的,比如注册了EventBus没注销,添加Activity到栈中,销毁的时候没移除等。
好了,以上就是比较常见的内存泄露原因和对应的解决方案,当然还有一些其他的,这里没有办法一一阐述,还是需要大家平时不断去积累,总结,这里提供一个可以检查内存泄露的工具LeakCanary,只需要几行代码就可以轻松在应用内集成内存监控功能了。