android开发中内存泄漏问题总结

android开发中内存泄漏问题总结


  java中存在让人省心的垃圾回收机制,但实际上内存泄漏仍然非常普遍,在应用中如果我们不再使用一个对象,但是仍然有隐藏着的引用指向这个对象,那么垃圾回收起就无法收回该对象,造成内存泄漏。android里内存泄漏不会对系统中其他应用的运行造成影响,原因是每个应用都会运行在一个独立的dalvik虚拟机进程里,android系统为不同类型的应用分配不同等级的内存上限,当运行超过这个上限时,android就会kill掉这个被认为发生内存泄漏的进程,报出OOM。

我遇到的OOM原因有以下几个:


  • 资源对象没有及时关闭
  • adapter的getView()没有使用缓存的convertView
  • 使用了非静态的内部类或匿名内部类
  • bitmap对象不再使用时没有调用recycle()方法
  • context的泄漏
    以上5类是我曾经遇到过的,当然还有其他类型的,暂时还没有遇到。下面分别对这5中类型做分析,并给出代码示例。

一. 资源对象没有及时关闭

  常见的忘记关闭的资源有:InputStream/OutStream、socket、File、
Cursor、还有线程对象,等待。当这些资源不再使用,或退出应用时都要及时关闭。


二. adapter的getView()没有使用缓存的convertView

  以构造ListView的BaseAdapter为例,在BaseAdapter中提供了方法:
public View getView(int position, View convertView, ViewGroup parent)

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

public View getView(int position, View convertView, ViewGroup parent) { 
  View view = new Xxx(...); //每一条item new一个对象,十亿个item就傻逼了
  ... ... 
  return view; 
} 

修改后的示例代码:

public View getView(int position, View convertView, ViewGroup parent) {
     inal int cc = position;
     ViewHolder holder = null;
     if (convertView == null) {
        holder = new ViewHolder();
        convertView = mInflater.inflate(R.layout.itemrow,null);
        holder.tagcolor = (ImageView) convertView.findViewById(R.id.itemrowiamge);
        holder.itemname = (TextView) convertView.findViewById(R.id.itemrowtext);
        holder.cb = (CheckBox) convertView.findViewById(R.id.itemrowcb);
        convertView.setTag(holder);
     } else {
     holder = (ViewHolder) convertView.getTag();
     }
     /**
     *给item 赋值操作...
     */
     return convertView;
}

三. 使用了非静态的内部类或匿名内部类
在Java中,非静态的内部类和匿名内部类都会隐式地持有其外部类的引用。静态的内部类不会持有外部类的引用。关于这一内容可以查看细话Java:”失效”的private修饰符

以下面的代码为例:

public class SampleActivity extends Activity {

  private final Handler mLeakyHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
      // ...
    }
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Post a message and delay its execution for 10 minutes.
    mLeakyHandler.postDelayed(new Runnable() {
      @Override
      public void run() { /* ... */ }
    }, 1000 * 60 * 10);

    // Go back to the previous Activity.
    finish();
  }
}

 分析一下上面的代码,当我们执行了Activity的finish方法,被延迟的消息会在被处理之前存在于主线程消息队列中10分钟,而这个消息中 又包含了Handler的引用,而Handler是一个匿名内部类的实例,其持有外面的SampleActivity的引用,所以这导致了 SampleActivity无法回收,进行导致SampleActivity持有的很多资源都无法回收,这就是我们常说的内存泄露。
注意上面的new Runnable这里也是匿名内部类实现的,同样也会持有SampleActivity的引用,也会阻止SampleActivity被回收。
要解决这种问题,思路就是不使用非静态内部类,继承Handler时,要么是放在单独的类文件中,要么就是使用静态内部类。因为静态的内部类不会持有外部类的引用,所以不会导致外部类实例的内存泄露。当你需要在静态内部类中调用外部的Activity时,我们可以使用弱引用来处理。

指向一个对象的强引用不为0时,System.gc();不会回收这个对象,若将弱引用也指向这个对象,就会实现这样一个效果:当强引用个数为0,System.gc()会回收被弱引用指向的对象。

  另外同样也需要将Runnable设置为静态的成员属性。注意:一个静态的匿名内部类实例不会持有外部类的引用。 修改后不会导致内存泄露的代码如下。
  

public class SampleActivity extends Activity {

  /**
   * Instances of static inner classes do not hold an implicit
   * reference to their outer class.
   */
  private static class MyHandler extends Handler {
    private final WeakReference<SampleActivity> mActivity;

    public MyHandler(SampleActivity activity) {
      mActivity = new WeakReference<SampleActivity>(activity);
    }

    @Override
    public void handleMessage(Message msg) {
      SampleActivity activity = mActivity.get();
      if (activity != null) {
        // ...
      }
    }
  }

  private final MyHandler mHandler = new MyHandler(this);

  /**
   * Instances of anonymous classes do not hold an implicit
   * reference to their outer class when they are "static".
   */
  private static final Runnable sRunnable = new Runnable() {
      @Override
      public void run() { /* ... */ }
  };

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Post a message and delay its execution for 10 minutes.
    mHandler.postDelayed(sRunnable, 1000 * 60 * 10);

    // Go back to the previous Activity.
    finish();
  }
}

其实在Android中很多的内存泄露都是由于在Activity中使用了非静态内部类导致的,就像本文提到的一样,所以当我们使用时要非静态内部 类时要格外注意,如果其实例的持有对象的生命周期大于其外部类对象,那么就有可能导致内存泄露。个人倾向于使用文章的静态类和弱引用的方法解决这种问题。
该部分参考了 这个网页的内容。


四. bitmap对象不再使用时没有调用recycle()方法

 在Android应用里,最耗费内存的就是图片资源。而且在Android系统中,读取位图Bitmap时,分给虚拟机中的图片的堆栈大小只有8M,如果超出了,就会出现OutOfMemory异常。所以,对于图片的内存优化,是Android应用开发中比较重要的内容。
 参考了这个网页
 Bitmap类有一个方法recycle(),从方法名可以看出意思是回收。这里就有疑问了,Android系统有自己的垃圾回收机制,可以不定期的回收掉不使用的内存空间,当然也包括Bitmap的空间。那为什么还需要这个方法呢?

Bitmap类的构造方法都是私有的,所以开发者不能直接new出一个Bitmap对象,只能通过BitmapFactory类的各种静态方法来实例化一个Bitmap。仔细查看BitmapFactory的源代码可以看到,生成Bitmap对象最终都是通过JNI调用方式实现的。所以,加载 Bitmap到内存里以后,是包含两部分内存区域的。简单的说,一部分是Java部分的,一部分是C部分的。这个Bitmap对象是由Java部分分配的,不用的时候系统就会自动回收了,但是那个对应的C可用的内存区域,虚拟机是不能直接回收的,这个只能调用底层的功能释放。所以需要调用 recycle()方法来释放C部分的内存。从Bitmap类的源代码也可以看到,recycle()方法里也的确是调用了JNI方法了的。

  • 及时释放Like This:
// 先判断是否已经回收

if(bitmap != null && !bitmap.isRecycled()){ 

        // 回收并且置为null

        bitmap.recycle(); 

        bitmap = null; 

} 

System.gc();
  • 图片缓存

     有时候,可能需要在一个Activity里多次用到同一张图片。比如一个Activity会展示一些用户的头像列表,而如果用户没有设置头像的话,则会显示一个默认头像,而这个头像是位于应用程序本身的资源文件中的。

     如果有类似上面的场景,就可以对同一Bitmap进行缓存。如果不进行缓存,尽管看到的是同一张图片文件,但是使用BitmapFactory类的方法来实例化出来的Bitmap,是不同的Bitmap对象。缓存可以避免新建多个Bitmap对象,避免内存的浪费。

经验分享:

Web开发者对于缓存技术是很熟悉的。其实在Android应用开发过程中,也会经常使用缓存的技术。这里所说的缓存有两个级别,一个是硬盘缓存,一个是内存缓存。比如说,在开发网络应用过程中,可以将一些从网络上获取的数据保存到SD卡中,下次直接从SD卡读取,而不从网络中读取,从而节省网络流量。这种方式就是硬盘缓存。再比如,应用程序经常会使用同一对象,也可以放到内存中缓存起来,需要的时候直接从内存中读取。这种方式就是内存缓存。 
  • 压缩图片

如果图片像素过大,使用BitmapFactory类的方法实例化Bitmap的过程中,需要大于8M的内存空间,就必定会发生OutOfMemory异常。这个时候该如何处理呢?如果有这种情况,则可以将图片缩小,以减少载入图片过程中的内存的使用,避免异常发生。


五. context的泄漏
  Context泄露,这一种属于第二种情况,但是由于太常见了,所以单拿出来,希望自己能予以重视,是由内部线程造成的。

 public class BasicActivity extends Activity {     
   @Override     
   public void onCreate(Bundle savedInstanceState) {     
       super.onCreate(savedInstanceState);     
       setContentView(R.layout.main);     
       new MyThread().start();     
   }     


   private class OneThread extends Thread{     
       @Override     
       public void run() {     
           super.run();     
           //do somthing takes long time...     
       }     
   }     
}  

  这段代码很平常也很简单,是我们经常使用的形式。我们思考一个问题:假设OneThread的run函数是一个很费时的操作,当我们开启该线程后,将设备的横屏变为了竖屏,一般情况下当屏幕转换时会重新创建Activity,按照我们的想法,老的Activity应该会被销毁才对,然而事实上并非如此。
  由于我们的线程是Activity的非静态内部类,所以OneThread中保存了Activity的一个引用,当OneThread的run函数没有结束时,OneThread是不会被销毁的,因此它所引用的老的Activity也不会被销毁,因此就出现了内存泄露的问题。
  那么上述内存泄露问题应该如何解决呢? 和第二类问题的解决办法一样。
第一、将线程的内部类,改为静态内部类。并且注意第二条。
第二、在线程内部采用弱引用保存Context引用。

结束语

  本文总结了个人项目中遇到的内存泄漏的情况,肯定还有其他类型的,希望本文能给大家切实带来帮助,也给自己提醒,如有发现错误或不足,请斧正。

猜你喜欢

转载自blog.csdn.net/xialingming/article/details/81296002