天天看点

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

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

Android leakcanary内存泄漏检测和一般的解决方案
Android 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等;

继续阅读