一、什么是内存泄漏
内存泄漏是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
二、内存泄漏场景
简单判断是否可能出现内存泄漏,A类实例引用B类实例,而A类实例的生命周期长于B类实例的生命周期。
网上很多说单例会导致内存泄漏,我是保持意见
1.Context类型参数避免用当前Activity.this,尽量用getApplicationContext()
代码语言:javascript复制public class YOU {
static Context context;
public static void setContext(Context c){
context=c;
}
}
然后在activity中调用setContext方法。这个时候参数尽量避免用Activity.this,如果用该参数,当前activiy实例会被YOU中的context持有,当activity关闭的时候会出现内存泄漏。此时应该用getApplicationContext()。
网上很多说单例会导致内存泄漏,但是他们用的实例都是持有Context,也就持有了当前的实例。持有activity其他字段的话,就不会出现内存泄漏。
2.在外部类结束的时候同时也结束内部类。
内部类会持有外部类的引用,当外部类实例准备回收的时候,遇到内部类持有外部类实例引用,此时外部类无法被回收。比如我们程序中用的Handler,Handler是内部类,在handlerMessage还没结束时,持有外部类activity实例,此时如果activity关闭,但是会出现内存泄漏。
代码语言:javascript复制private class MHandler extends Handler{
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
在activity里面定义MHandler且调用了其实例
代码语言:javascript复制MHandler h=new MHandler();
h.sendEmptyMessage(200);
在退出当前activity时,假如handleMessage还在执行任务,MHandler还持有activity引用,那就出现activity内存泄漏。
这里稍微引申下,这里MHandler持有activity引用,谁持有MHandler,是Message,那Message间接持有activity,当activity退出时,如果MessageQueue里面的Message还在处理,那activity就内存泄漏。MessageQueue源码可以看下 Android源码解读-Handler、Loop、MessageQueue 这篇文章。
有的时候,我们无法避免内部类,在外部类结束前就把任务处理完成。那我们就要在外部类结束前把内部类任务处理完成。
如何做:
1.把MessageQueue消息清空
2.MHandler 改为静态类(静态内部类不持有外部类),同时加上弱引用,如果activity不是弱引用,即使Handler是static的,但是还是会引用当前activity对象。【如果不是Handler不是静态的,那Handler就持有activity;如果Handler中Activity对象不是弱引用,那Handler中Activity对象会持有当前activity实例】
至于内部类和静态内部类和外部类的关系,后面来一篇文章
3.资源未关闭或释放导致内存泄露
这个比较好理解,我们在操作文件或数据库完成后,要及时关闭。如果不及时关闭,这些缓冲对象就会一直被占用而得不到释放,以致发生内存泄露。
4.未取消注册或回调导致内存泄露
比如我们在Activity中注册广播,如果在Activity销毁后不取消注册,那么这个刚播会一直存在系统中,同上面所说的非静态内部类一样持有Activity引用,导致内存泄露。因此注册广播后在Activity销毁后一定要取消注册。
5.属性动画造成内存泄露
动画是一个耗时任务,比如在Activity中启动了属性动画(ObjectAnimator),但是在销毁的时候,没有调用cancle方法,那在退出activity时,动画并没有停止,还在不断运动,动画引用所在的控件,所在的控件引用Activity,同样会导致activity内存泄漏。
6.WebView造成内存泄露
WebView的内存泄露,因为WebView在加载网页后会长期占用内存而不能被释放,因此我们在Activity销毁后要调用它的destory()方法来销毁它以释放内存。另外在查阅WebView内存泄露相关资料时看到这种情况:Webview下面的Callback持有Activity引用,造成Webview内存无法释放,即使是调用了Webview.destory()等方法都无法解决问题。最终的解决方案是:在销毁WebView之前需要先将WebView从父容器中移除,然后再销毁WebView。
代码语言:javascript复制@Override
protected void onDestroy() {
if( mWebView!=null) {
// 如果先调用destroy()方法,则会命中if (isDestroyed()) return;这一行代码,需要先onDetachedFromWindow(),再
// destory()
ViewParent parent = mWebView.getParent();
if (parent != null) {
((ViewGroup) parent).removeView(mWebView);
}
mWebView.stopLoading();
// 退出时调用此方法,移除绑定的服务,否则某些特定系统会报错
mWebView.getSettings().setJavaScriptEnabled(false);
mWebView.clearHistory();
mWebView.clearView();
mWebView.removeAllViews();
mWebView.destroy();
}
super.on Destroy();
}
7.集合中的对象未清理造成内存泄露
这个比较好理解,如果一个对象放入到ArrayList、HashMap等集合中,这个集合就会持有该对象的引用。当我们不再需要这个对象时,也并没有将它从集合中移除,这样只要集合还在使用(而此对象已经无用了),这个对象就造成了内存泄露。并且如果集合被静态引用的话,集合里面那些没有用的对象更会造成内存泄露了。所以在使用集合时要及时将不用的对象从集合remove,或者clear集合,以避免内存泄漏,例如EventBus就退出activity时,要调用unregister方法
三、内存泄漏处理工具
这方面我们有多种方案,我这边常用的是AndroidStudio 中的Profiler 和 Leakcancary,这里我主要简单说下Profiler使用流程。
在我们运行程序过程中,如果出现重复操作导致内存不断增大,而且手动gc没有明显效果,我们就要怀疑是否出现内存泄漏问题。
这个时候我们可以执行这两步。
我们在红色1手动gc后,gc点后面选中一个时间点的内存区域,然后执行红色2按钮,产生dump文件,得到下面结果。
1.表示有几个内存泄漏
2.查看当前的内存泄漏对象,我们可以先关注自己的工程的内存泄漏.
3.是对象实例,可能这个对象有多个泄漏对象
4.代表引用
5.点击5以后,6就可以很容易定位到是什么变量导致的这个对象的内存泄漏
通过上图我们可以大概知道是哪里出现内存泄漏,Profiler更多使用细节可以网上查查,很多。