Android leakcanary内存泄漏检测和一般的解决方案

移动开发 来源:qq_17338093 873℃ 1评论
检测内存泄漏工具对比:
MAT:Java堆内存分析工具;和eclipse很像;
YourKit:第三方收费软件,检测java c#程序性能;
LeakCanary:能保存内存镜像文件;
LeakCanary和MAT的区别:
    a.使用简单;
    b.显示效果方便;
本文主要介绍 leakcanary


使用:
github地址: 点击打开链接
dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4'
   testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4'
 }
在application中:
LeakCanary.install(this);

1、单例造成的内存泄漏
Android的单例模式非常受开发者的喜爱,不过使用的不恰当的话也会造成内存泄漏。
因为单例的静态特性使得单例的生命周期和应用的生命周期一样长,
这就说明了如果一个对象已经不需要使用了,而单例对象还持有该对象的引用,那么这个对象将不能被正常回收,这就导致了内存泄漏。
如下这个典例:
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,所以这个Context的生命周期的长短至关重要:
1)、传入的是Application的Context:这将没有任何问题,因为单例的生命周期和Application的一样长;
2)、传入的是Activity的Context:当这个Context所对应的Activity退出时,由于该Context和Activity的生命周期一样长(Activity间接继承于Context),所以当前Activity退出时它的内存并不会被回收,因为单例对象持有该Activity的引用。
所以正确的单例应该修改为下面这种方式:
public class AppManager {  
    private static AppManager instance;  
    private Context context;  
    private AppManager(Context context) {  
        this.context = context.getApplicationContext();  
    }  
    public static AppManager getInstance(Context context) {  
        if (instance != null) {  
            instance = new AppManager(context);  
        }  
        return instance;  
    }  
}
不管传入什么Context最终将使用Application的Context,而单例的生命周期和应用的一样长,这样就防止了内存泄漏。
2、Handler造成的内存泄漏
Handler的使用造成的内存泄漏问题应该说最为常见了,
平时在处理网络任务或者封装一些请求回调等api都应该会借助Handler来处理,
对于Handler的使用代码编写一不规范即有可能造成内存泄漏,如下示例:
Handler mHandler = new Handler() {  
    @Override  
    public void handleMessage(Message msg) {  
        mImageView.setImageBitmap(mBitmap);  
    }  
}
上面是一段简单的Handler的使用。当使用内部类(包括匿名类)
来创建Handler的时候,Handler对象会隐式地持有一个外部类对象
(通常是一个Activity)的引用(不然你怎么可能通过Handler来操作Activity中的View?)。
而Handler通常会伴随着一个耗时的后台线程(例如从网络拉取图片)一起出现,这个后台线程在任务执行完毕(例如图片下载完毕)之后,通过消息机制通知Handler,然后Handler把图片更新到界面。然而,如果用户在网络请求过程中关闭了Activity,正常情况下,Activity不再被使用,它就有可能在GC检查时被回收掉,但由于这时线程尚未执行完,而该线程持有Handler的引用(不然它怎么发消息给Handler?),这个Handler又持有Activity的引用,就导致该Activity无法被回收(即内存泄露),直到网络请求结束(例如图片下载完毕)。另外,如果你执行了Handler的postDelayed()方法:
//要做的事情,这里再次调用此Runnable对象,以实现每两秒实现一次的定时器操作
handler.postDelayed(this, 2000);
该方法会将你的Handler装入一个Message,并把这条Message推到MessageQueue中,那么在你设定的delay到达之前,会有一条MessageQueue -> Message -> Handler -> Activity的链,导致你的Activity被持有引用而无法被回收。
这种创建Handler的方式会造成内存泄漏,由于mHandler是Handler的非静态匿名内部类的实例,所以它持有外部类Activity的引用,我们知道消息队列是在Looper中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏。
使用Handler导致内存泄露的解决方法
方法一:通过程序逻辑来进行保护。
1).在关闭Activity的时候停掉你的后台线程。线程停掉了,就相当于切断了Handler和外部连接的线,Activity自然会在合适的时候被回收。
2).如果你的Handler是被delay的Message持有了引用,那么使用相应的Handler的removeCallbacks()方法,把消息对象从消息队列移除就行了。
方法二:将Handler声明为静态类。
静态类不持有外部类的对象,所以你的Activity可以随意被回收。代码如下:
static class MyHandler extends Handler {  
    @Override  
    public void handleMessage(Message msg) {  
        mImageView.setImageBitmap(mBitmap);  
    }  
}
但其实没这么简单。使用了以上代码之后,你会发现,由于Handler不再持有外部类对象的引用,导致程序不允许你在Handler中操作Activity中的对象了。所以你需要在Handler中增加一个对Activity的弱引用(WeakReference):
static class MyHandler extends Handler {  
    WeakReference<Activity > mActivityReference;  
    MyHandler(Activity activity) {  
        mActivityReference= new WeakReference<Activity>(activity);  
    }  
    @Override  
    public void handleMessage(Message msg) {  
        final Activity activity = mActivityReference.get();  
        if (activity != null) {  
            mImageView.setImageBitmap(mBitmap);  
        }  
    }  
}
将代码改为以上形式之后,就算完成了。
延伸:什么是WeakReference?
WeakReference弱引用,与强引用(即我们常说的引用)相对,它的特点是,GC在回收时会忽略掉弱引用,即就算有弱引用指向某对象,但只要该对象没有被强引用指向(实际上多数时候还要求没有软引用,但此处软引用的概念可以忽略),该对象就会在被GC检查到时回收掉。对于上面的代码,用户在关闭Activity之后,就算后台线程还没结束,但由于仅有一条来自Handler的弱引用指向Activity,所以GC仍然会在检查的时候把Activity回收掉。
这样,内存泄露的问题就不会出现了。
4、线程造成的内存泄漏
对于线程造成的内存泄漏,也是平时比较常见的,如下这两个示例可能每个人都这样写过:
//——————test1  
        new AsyncTask<Void, Void, Void>() {  
            @Override  
            protected Void doInBackground(Void... params) {  
                SystemClock.sleep(10000);  
                return null;  
            }  
        }.execute();  
//——————test2  
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                SystemClock.sleep(10000);  
            }  
        }).start();
上面的异步任务和Runnable都是一个匿名内部类,因此它们对当前Activity都有一个隐式引用。如果Activity在销毁之前,任务还未完成, 那么将导致Activity的内存资源无法回收,造成内存泄漏。正确的做法还是使用静态内部类的方式,如下:
static class MyAsyncTask extends AsyncTask<Void, Void, Void> {  
        private WeakReference<Context> weakReference;  

        public MyAsyncTask(Context context) {  
            weakReference = new WeakReference<>(context);  
        }  

        @Override  
        protected Void doInBackground(Void... params) {  
            SystemClock.sleep(10000);  
            return null;  
        }  

        @Override  
        protected void onPostExecute(Void aVoid) {  
            super.onPostExecute(aVoid);  
            MainActivity activity = (MainActivity) weakReference.get();  
            if (activity != null) {  
                //...  
            }  
        }  
    }  
    static class MyRunnable implements Runnable{  
        @Override  
        public void run() {  
            SystemClock.sleep(10000);  
        }  
    }  
//——————  
    new Thread(new MyRunnable()).start();  
    new MyAsyncTask(this).execute(); 
通过上面的代码,新线程再也不会持有一个外部Activity 的隐式引用,而且该Activity也会在配置改变后被回收。这样就避免了Activity的内存资源泄漏,当然在Activity销毁时候也应该取消相应的任务AsyncTask::cancel(),避免任务在后台执行浪费资源。
如果我们线程做的是一个无线循环更新UI的操作,如下代码:
private static class MyThread extends Thread {  
        @Override  
        public void run() {  
          while (true) {  
            SystemClock.sleep(1000);  
          }  
        }  
      } 
这样虽然避免了Activity无法销毁导致的内存泄露,但是这个线程却发生了内存泄露。在Java中线程是垃圾回收机制的根源,也就是说,在运行系统中DVM虚拟机总会使硬件持有所有运行状态的进程的引用,结果导致处于运行状态的线程将永远不会被回收。因此,你必须为你的后台线程实现销毁逻辑!下面是一种解决办法:
private static class MyThread extends Thread {  
        private boolean mRunning = false;  

        @Override  
        public void run() {  
          mRunning = true;  
          while (mRunning) {  
            SystemClock.sleep(1000);  
          }  
        }  

        public void close() {  
          mRunning = false;  
        }  
      } 
在Activity退出时,可以在 onDestroy()方法中显示调用mThread.close();以此来结束该线程,这就避免了线程的内存泄漏问题。
5、资源对象没关闭造成的内存泄漏
资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏。因为有些资源性对象,比如SQLiteCursor(在析构函数finalize(),如果我们没有关闭它,它自己会调close()关闭),如果我们没有关闭它,系统在回收它时也会关闭它,但是这样的效率太低了。因此对于资源性对象在不使用的时候,应该调用它的close()函数,将其关闭掉,然后才置为null.在我们的程序退出时一定要确保我们的资源性对象已经关闭。
程序中经常会进行查询数据库的操作,但是经常会有使用完毕Cursor后没有关闭的情况。如果我们的查询结果集比较小,对内存的消耗不容易被发现,只有在常时间大量操作的情况下才会复现内存问题,这样就会给以后的测试和问题排查带来困难和风险。
示例代码:
Cursor cursor = getContentResolver().query(uri...);    
if (cursor.moveToNext()) {    
  ... ...      
}

修正示例代码:  

Cursor cursor = null;    
try {    
  cursor = getContentResolver().query(uri...);    
  if (cursor != null &&cursor.moveToNext()) {    
      ... ...      
  }    
} finally {    
  if (cursor != null) {    
      try {      
          cursor.close();    
      } catch (Exception e) {    
          //ignore this     
      }    
   }    
}
6、Bitmap没有回收导致的内存溢出
Bitmap的不当处理极可能造成OOM,绝大多数情况都是因这个原因出现的。Bitamp位图是Android中当之无愧的胖小子,所以在操作的时候当然是十分的小心了。由于Dalivk并不会主动的去回收,需要开发者在Bitmap不被使用的时候recycle掉。使用的过程中,及时释放是非常重要的。同时如果需求允许,也可以去BItmap进行一定的缩放,通过BitmapFactory.Options的inSampleSize属性进行控制。如果仅仅只想获得Bitmap的属性,其实并不需要根据BItmap的像素去分配内存,只需在解析读取Bmp的时候使用BitmapFactory.Options的inJustDecodeBounds属性。最后建议大家在加载网络图片的时候,使用软引用或者弱引用并进行本地缓存,推荐使用android-universal-imageloader或者xUtils等;