Android中内存泄露与如何有效避免OOM总结

1、关于OOM与内存泄露的概念

       咱们在Android开发过程当中常常会遇到OOM的错误,这是由于咱们在APP中没有考虑dalvik虚拟机内存消耗的问题。java

       一、什么是OOM

          OOM:即OutOfMemoery,顾名思义就是指内存溢出了。内存溢出是指APP向系统申请超过最大阀值的内存请求,系统不会再分配多余的空间,就会形成OOM error。在咱们Android平台下,多数状况是出如今图片不当处理加载的时候。android

          Android系统为每一个应用程序分配的内存有限,当一个应用中产生的内存泄漏比较多时,就不免会致使应用所须要的内存超过这个系统分配的内存限额,这就形成了内存溢出而致使应用Crash。Android APP的所能申请的最大内存大小是多少,有人说是16MB,有人又说是24MB。其实这些答案都算对,由于Android是开源的操做系统,不一样的手机厂商实际上是拥有修改这部分权限能力的,因此就形成了不一样品牌和不一样系统的手机,对于APP的内存支持也是不同的,不过咱们能够经过Runtime这个类来获取当前设备的Android系统为每一个应用所产生的内存大小。APP并不会为咱们建立Runtime的实例,Java为咱们提供了单例获取的方式Runtime.getRuntime()。经过maxMemory()方法获取系统可为APP分配的最大内存,totalMemory()获取APP当前所分配的内存heap空间大小。算法

 

       二、什么是内存泄露

 

       Java使用有向图机制,经过GC自动检查内存中的对象(何时检查由虚拟机决定),若是GC发现一个或一组对象为不可到达状态,则将该对象从内存中回收。也就是说,一个对象不被任何引用所指向,则该对象会在被GC发现的时候被回收;另外,若是一组对象中只包含互相的引用,而没有来自它们外部的引用(例若有两个对象A和B互相持有引用,但没有任何外部对象持有指向A或B的引用),这仍然属于不可到达,一样会被GC回收。数据库

      在Android程序开发中,当一个对象已经不须要再使用了,本该被回收时,而另一个正在使用的对象持有它的引用从而致使它不能被回收,这就致使本该被回收的对象不能被回收而停留在堆内存中,内存泄漏就产生了。api

      内存泄露的危害:只有一个,那就是虚拟机占用内存太高,致使OOM(内存溢出),程序出错。对于Android应用来讲,就是你的用户打开一个Activity,使用完以后关闭它,内存泄露;又打开,又关闭,又泄露;几回以后,程序占用内存超过系统限制,FC。缓存

      了解了内存泄漏的缘由及影响后,咱们须要作的就是掌握常见的内存泄漏,并在之后的Android程序开发中,尽可能避免它。安全

2、常见的内存泄漏及解决方案

     一、单例形成的内存泄漏

     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,而单例的生命周期和应用的同样长,这样就防止了内存泄漏。

     二、非静态内部类建立静态实例形成的内存泄漏

     在Java 中,非静态匿名内部类会持有其外部类的隐式引用,若是你没有考虑过这一点,那么存储该引用会致使Activity被保留,而不是被垃圾回收机制回收。Activity对象持有其View层以及相关联的全部资源文件的引用,换句话说,若是你的内存泄漏发生在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(mManager == null){    
            mManager = new TestResource();    
        }    
        //...    
    }    
    class TestResource {    
        //...    
    }    
}

这样就在Activity内部建立了一个非静态内部类的单例,每次启动Activity时都会使用该单例的数据,这样虽然避免了资源的重复建立,不过这种写法却会形成内存泄漏,由于非静态内部类默认会持有外部类的引用,而又使用了该非静态内部类建立了一个静态的实例,该实例的生命周期和应用的同样长,这就致使了该静态实例一直会持有该Activity的引用,致使Activity的内存资源不能正常回收。正确的作法为:

      将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,若是须要使用Context,请使用ApplicationContext。

     三、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回收掉。这样,内存泄露的问题就不会出现了。

     四、线程形成的内存泄漏

     对于线程形成的内存泄漏,也是平时比较常见的,以下这两个示例可能每一个人都这样写过:

//——————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();以此来结束该线程,这就避免了线程的内存泄漏问题。

     五、资源对象没关闭形成的内存泄漏

     资源性对象好比(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       
      }      
   }      
}  

六、Bitmap没有回收致使的内存溢出

     Bitmap的不当处理很可能形成OOM,绝大多数状况都是因这个缘由出现的。Bitamp位图是Android中当之无愧的胖小子,因此在操做的时候固然是十分的当心了。因为Dalivk并不会主动的去回收,须要开发者在Bitmap不被使用的时候recycle掉。使用的过程当中,及时释放是很是重要的。同时若是需求容许,也能够去BItmap进行必定的缩放,经过BitmapFactory.Options的inSampleSize属性进行控制。若是仅仅只想得到Bitmap的属性,其实并不须要根据BItmap的像素去分配内存,只需在解析读取Bmp的时候使用BitmapFactory.Options的inJustDecodeBounds属性。最后建议你们在加载网络图片的时候,使用软引用或者弱引用并进行本地缓存,推荐使用android-universal-imageloader或者xUtils,牛人出品,必属精品。

七、构造Adapter时,没有使用缓存的convertView

     以构造ListView的BaseAdapter为例,在BaseAdapter中提供了方法:

public View getView(int position, ViewconvertView, ViewGroup parent) 

来向ListView提供每个item所须要的view对象。初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化必定数量的view对象,同时ListView会将这些view对象缓存起来。当向上滚动ListView时,原先位于最上面的list item的view对象会被回收,而后被用来构造新出现的最下面的list item。这个构造过程就是由getView()方法完成的,getView()的第二个形参View convertView就是被缓存起来的list item的view对象(初始化时缓存中没有view对象则convertView是null)。由此能够看出,若是咱们不去使用convertView,而是每次都在getView()中从新实例化一个View对象的话,即浪费资源也浪费时间,也会使得内存占用愈来愈大。ListView回收list item的view对象的过程能够查看:

     Android.widget.AbsListView.java --> voidaddScrapView(View scrap)方法。

     示例代码:

public View getView(int position, ViewconvertView, ViewGroup parent) {      
  View view = new Xxx(...);      
  ... ...      
  return view;      
}      
     修正示例代码:  
  
public View getView(int position, ViewconvertView, ViewGroup parent) {      
  View view = null;      
  if (convertView != null) {      
  view = convertView;      
  populate(view, getItem(position));      
  ...      
  } else {      
  view = new Xxx(...);      
  ...      
  }      
  return view;      
}

3、预防OOM的几点建议

     Android开发过程当中,在 Activity的生命周期里协调耗时任务可能会很困难,你一不当心就会致使内存泄漏问题。下面是一些小提示,能帮助你预防内存泄漏问题的发生:

     一、合理使用static:

     每个非静态内部类实例都会持有一个外部类的引用,若该引用是Activity 的引用,那么该Activity在被销毁时将没法被回收。若是你的静态内部类须要一个相关Activity的引用以确保功能可以正常运行,那么你得确保你在对象中使用的是一个Activity的弱引用,不然你的Activity将会发生意外的内存泄漏。可是要注意,当此类在全局多处用到时在这样干,由于static声明变量的生命周期实际上是和APP的生命周期同样的,有点相似与Application。若是大量的使用的话,就会占据内存空间不释放,聚沙成塔也会形成内存的不断开销,直至挂掉。static的合理使用通常用来修饰基本数据类型或者轻量级对象,尽可能避免修复集合或者大对象,经常使用做修饰全局配置项、工具类方法、内部类。

     二、善用SoftReference/WeakReference/LruCache

     Java、Android中有没有这样一种机制呢,当内存吃紧或者GC扫过的状况下,就能及时把一些内存占用给释放掉,从而分配给须要分配的地方。答案是确定的,java为咱们提供了两个解决方案。若是对内存的开销比较关注的APP,能够考虑使用WeakReference,当GC回收扫过这块内存区域时就会回收;若是不是那么关注的话,可使用SoftReference,它会在内存申请不足的状况下自动释放,一样也能解决OOM问题。同时Android自3.0之后也推出了LruCache类,使用LRU算法就释放内存,同样的能解决OOM,若是兼容3.0一下的版本,请导入v4包。关于第二条的无关引用的问题,咱们传参能够考虑使用WeakReference包装一下。

     三、谨慎handler

     在处理异步操做的时候,handler + thread是个不错的选择。可是相信在使用handler的时候,你们都会遇到警告的情形,这个就是lint为开发者的提醒。handler运行于UI线程,不断处理来自MessageQueue的消息,若是handler还有消息须要处理可是Activity页面已经结束的状况下,Activity的引用其实并不会被回收,这就形成了内存泄漏。解决方案,一是在Activity的onDestroy方法中调handler.removeCallbacksAndMessages(null);取消全部的消息的处理,包括待处理的消息;二是声明handler的内部类为static。

     四、不要总想着Java 的垃圾回收机制会帮你解决全部内存回收问题

     就像上面的示例,咱们觉得垃圾回收机制会帮咱们将不须要使用的内存回收,例如:咱们须要结束一个Activity,那么它的实例和相关的线程都该被回收。但现实并不会像咱们剧本那样走。Java线程会一直存活,直到他们都被显式关闭,抑或是其进程被Android系统杀死。因此,为你的后台线程实现销毁逻辑是你在使用线程时必须时刻铭记的细节,此外,你在设计销毁逻辑时要根据Activity的生命周期去设计,避免出现Bug。

     考虑你是否真的须要使用线程。Android应用的框架层为咱们提供了不少便于开发者执行后台操做的类。例如:咱们可使用Loader 代替在Activity 的生命周期中用线程经过注入执行短暂的异步后台查询操做,考虑用Service将结构通知给UI的BroadcastReceiver。最后,记住,这篇博文中对线程进行的讨论一样适用于AsyncTask(由于AsyncTask使用ExecutorService执行它的任务)。然而,虽然说ExecutorService只能在短暂操做(文档说最多几秒)中被使用,那么这些方法致使的Activity内存泄漏应该永远不会发生。

     五、ListView和GridView的item缓存

     对于移动设备,尤为硬件良莠不齐的android生态,页面的绘制实际上是很耗时的,findViewById也是蛮慢的。因此不重用View,在有列表的时候就尤其显著了,常常会出现滑动很卡的现象,因此咱们要善于重复利用建立好的控件。这里主要注意两点:

     1)convertView重用

     ListView中的每个Item显示都须要Adapter调用一次getView()的方法,这个方法会传入一个convertView的参数,这个方法返回的View就是这个Item显示的View。Android提供了一个叫作Recycler(反复循环)的构件,就是当ListView的Item从滚出屏幕视角以外,对应Item的View会被缓存到Recycler中,相应的会从生成一个Item,而此时调用的getView中的convertView参数就是滚出屏幕的缓存Item的View,因此说若是能重用这个convertView,就会大大改善性能。

     2)使用ViewHolder重用

     咱们都知道在getView()方法中的操做是这样的:先从xml中建立view对象(inflate操做,咱们采用了重用convertView方法优化),而后在这个view去findViewById,找到每个item的子View的控件对象,如:ImageView、TextView等。这里的findViewById操做是一个树查找过程,也是一个耗时的操做,因此这里也须要优化,就是使用ViewHolder,把每个item的子View控件对象都放在Holder中,当第一次建立convertView对象时,便把这些item的子View控件对象findViewById实例化出来并保存到ViewHolder对象中。而后用convertView的setTag将viewHolder对象设置到Tag中, 当之后加载ListView的item时即可以直接从Tag中取出复用ViewHolder对象中的,不须要再findViewById找item的子控件对象了。这样便大大提升了性能。

     不过Android5.L为咱们提供了RecyclerView,RecyclerView是经典的ListView的进化与升华,它比ListView更加灵活,但也所以引入了必定的复杂性。最新的v7支持包新添加了RecyclerView。RecyclerView提供了一种插拔式的体验,高度的解耦,异常的灵活,经过设置它提供的不一样LayoutManager,ItemDecoration , ItemAnimator实现使人瞠目的效果。并且RecyclerView内部为咱们处理了item缓存,因此用着效率更高,更安全,感兴趣的读者能够了解一下。