内存泄露的原因
内存泄露是指不在需要的对象仍然被引用不能被GC回收释放,这句话你可能看到过不止一遍了,下面我们来深入研究一下这句话。
首先了解一下两个名词:
- GC: 垃圾回收器,会自动回收不在被引用的内存数据
- GC Roots:不能被回收的对象(这里的解释不是很好,往下看就明白了)
下面我们来看一张图:
GC Roots持有的对象都不能被垃圾回收器回收,所以这里的Object D不能被回收,Object G可以被回收,那么那些对象可以作为GC Roots呢,有以下对象:
- JavaStack(栈)中的引用的对象
- 方法区中静态引用指向的对象
- 方法区中常量引用指向的对象
- Native方法中JNI引用的对象
- Thread——活着的线程
可能现在你对这些还不是很明白,不要着急,下面我们讲具体的内存泄露的例子你就清楚了。
1.单例模式导致的内存泄露
直接上代码:
public class MyUtil {
private volatile static MyUtil myUtil;
private Context context;
private MyUtil(Context context) {
this.context = context;
}
public static MyUtil getMyUtil(Context context) {
if (myUtil == null) {
synchronized (MyUtil.class){
if (myUtil == null) {
myUtil = new MyUtil(context);
}
}
}
return myUtil;
}
}
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyUtil myUtil = MyUtil.getMyUtil(this);
}
}
可以看到上面的MyUtil是以一个很普遍的单例类并且在第一次创建的时候传入context,然后在Activity中初始化这个单例,这样单例对象级持有了这个Activity的引用,当退出Activity的时候由于被单例对象所持有就不能被GC回收,这就造成了内存泄漏。解决方法也很简单,在application中初始化就可以了,这样单例对象会持有application的引用。
注:其实这就是我们上面说的GC Roots中的静态引用,静态对象引用context导致对象不能被回收。
2.匿名内部类导致的内存泄露
其实内部类并不会直接导致内存泄露,而是很多情况下我们使用不当导致的。
我们知道内部类会持有外部类的引用,当内部类对象被GC Roots直接或间接持有导致不能回收释放的时候外部类也会被连带着不能回收释放,这就是导致内存泄露的原因。
我们看一个非常常见的例子:
public class MainActivity extends AppCompatActivity {
private Handler handler=new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
handler.postDelayed(new Runnable() {
@Override
public void run() {
}
}, 1000 * 60);
}
}
这里我们创建了一个匿名内部类并实例化了一个对象handler,然后在Activity中发送一个延时事件,这时退出当前Activity会发生内存泄露吗,如果延时任务已经结束就不会内存泄漏,如果延时任务没有结束就会发生内存泄露,因为当该 Activity 被 finish() 掉时,延迟执行任务的 Message 还会继续存在于主线程中,它持有该 Activity 的 Handler 引用,然后又因 为 Handler 为匿名内部类,它会持有外部类的引用,所以此时 finish() 掉的 Activity 就不会被回收了,从而造成内存泄漏。
修复方法:在 Activity 中避免使用非静态内部类或匿名内部类,比如将 Handler 声明为静态的,则其存活期跟 Activity 的生命周期就无关了。如果需要用到Activity,就通过弱引用的方式引入 Activity,避免直接将 Activity 作为 context 传进去。另外, Looper 线程的消息队列中还是可能会有待处理的消息,所以我们在 Activity 的 Destroy 时或者 Stop 时应该移除消息队列 MessageQueue 中的消息。
public class MainActivity extends AppCompatActivity {
private MyHandler handler
private static class MyHandler extends Handler {
private WeakReference<MainActivity> activityWeakReference;
public MyHandler(MainActivity activity) {
activityWeakReference = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
MainActivity activity = activityWeakReference.get();
if (activity != null) {
//do something
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
handler = new MyHandler(this);
handler.postDelayed(new Runnable() {
@Override
public void run() {
}
}, 1000 * 60);
}
@Override
protected void onDestroy() {
super.onDestroy();
handler.removeCallbacksAndMessages(null);
}
}
关于引用类型这里提一下:
Java中的引用有四种,分别为强引用,软引用,弱引用,虚引用。
- 强引用(Strong References):强应用的对象永远不会被回收,即使系统内存溢出也不会回收
- 软应用(Soft References):只有当系统的内存不够的情况下才会去回收
- 弱引用(Weak References):弱引用会被jvm忽略,也就说在GC进行垃圾收集的时候,如果一个对象只有弱引用指向它,那么和没有引用指向它是一样的效果,jvm都会对它就行果断的销毁,释放内存。
- 虚引用(Phantom References):虚幻应用和弱引用的回收机制差不多,都是可以被随时回收的。但是不同的地方是,它的构造方法必须强制传入ReferenceQueue,因为在jvm回收前(重点: 对,就是回收前,软引用和弱引用都是回收后),会将PhantomReference对象加入ReferenceQueue中; 还有一点就是PhantomReference.get()方法永远返回空,不管对象有没有被回收。
3.InputMethodManager导致的内存泄露
InputMethodManager imm = (InputMethodManager) v.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
当我们在代码中获取InputMethodManager之后再退出当前的页面就会发生内存泄漏,这是Android的一个bug,直到Android6.0之后才修复,下面我们看一下解决办法:
public static void fixSoftInputLeaks(final Context context) {
if (context == null) return;
InputMethodManager imm =
(InputMethodManager) Utils.getApp().getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm == null) return;
String[] strArr = new String[]{"mCurRootView", "mServedView", "mNextServedView"};
for (int i = 0; i < 3; i++) {
try {
Field declaredField = imm.getClass().getDeclaredField(strArr[i]);
if (declaredField == null) continue;
if (!declaredField.isAccessible()) {
declaredField.setAccessible(true);
}
Object obj = declaredField.get(imm);
if (obj == null || !(obj instanceof View)) continue;
View view = (View) obj;
if (view.getContext() == context) {
declaredField.set(imm, null);
} else {
return;
}
} catch (Throwable th) {
th.printStackTrace();
}
}
}
这里是通过反射将InputMethodManager中的”mCurRootView”, “mServedView”, “mNextServedView”三个变量致空。
检测内存泄漏的工具
LeakCanary是Android平台一个非常好用的内存泄漏检测工具,我们只需要将他放到我们的项目中,然后打包app运行到手机上就能自动帮我们找到内存泄漏的位置,但是他也不是百分比能检测出来,而且有时可能还会出现检测出错的情况,但这丝毫不影响他好用的优点。
github地址
关于他的使用我这里就不多说了,网上有很多教程。
我们的Android studio其实也能很好的分析内存泄漏,再3.0的版本里叫做Android Profiler,关于他的使用可以参考这篇文章,讲的很详细。