天天看点

内存泄露详解

1.基本介绍

1.1什么是内存泄露?

Android进程中某些对象(垃圾对象)已经没有使用价值了,但是它们却可以直接或间接地被引用到,导致无法被GC回收。无用的对象占据着内存空间,使得实际可使用内存变小,导致应用所需要的内存超过这个系统分配的内存限额,这就造成了内存溢出而导致应用卡顿、最终Crash。

1.2为什么会产生内存泄漏?

android的每个应用程序都会使用一个专有的Dalvik虚拟机实例来运行,它是由Zygote服务进程孵化出来的,也就是说每个应用程序都是在属于自己的进程中运行的。Android为不同类型的进程分配了不同的内存使用上限(在不同的设备上会因为RAM大小不同而各有差异),如果程序在运行过程中出现了内存泄漏的而造成应用进程使用的内存超过了这个上限,则会被系统视为内存泄漏,从而被kill掉,这使得仅仅自己的进程被kill掉,而不会影响其他进程(如果是system_process等系统进程出问题的话,则会引起系统重启)。

1.3内存泄露的危害

  1. 导致用户手机可用内存变少
  2. 程序出现卡顿
  3. 导致应用莫名退出
  4. 应用程序Force Close
  5. 用户流失

2.Android中常见的内存泄漏

2.1资源使用了为关闭

BroadcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

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

在BaseAdapter中提供了方法:

public View getView(intposition, View convertView, 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–> void addScrapView(View scrap) 方法。

2.3注册没取消造成的内存泄漏

一些Android程序可能引用我们的Anroid程序的对象(比如注册机制)。即使我们的Android程序已经结束了,但是别的引用程序仍然还有对我们的Android程序的某个对象的引用,泄漏的内存依然不能被垃圾回收。调用registerReceiver后未调用unregisterReceiver。

比如:假设我们希望在锁屏界面(LockScreen)中,监听系统中的电话服务以获取一些信息(如信号强度等),则可以在LockScreen中定义一个 PhoneStateListener的对象,同时将它注册到TelephonyManager服务中。对于LockScreen对象,当需要显示锁屏界面的时候就会创建一个LockScreen对象,而当锁屏界面消失的时候LockScreen对象就会被释放掉。

但是如果在释放 LockScreen对象的时候忘记取消我们之前注册的PhoneStateListener对象,则会导致LockScreen无法被垃圾回收。如果不断的使锁屏界面显示和消失,则最终会由于大量的LockScreen对象没有办法被回收而引起OutOfMemory,使得system_process 进程挂掉。

虽然有些系统程序,它本身好像是可以自动取消注册的(当然不及时),但是我们还是应该在我们的程序中明确的取消注册,程序结束时应该把所有的注册都取消掉

2.4单例造成的内存泄漏

单例模式非常受开发者的喜爱,不过使用的不恰当的话也会造成内存泄漏,由于单例的静态特性使得单例的生命周期和应用的生命周期一样长,这就说明了如果一个对象已经不需要使用了,而单例对象还持有该对象的引用,那么这个对象将不能被正常回收,这就导致了内存泄漏。

创建这个单例的时候,由于需要传入一个Context,所以这个Context的生命周期的长短至关重要:

1. 传入的是Application的Context:这将没有任何问题,因为单例的生命周期和Application的一样长

2. 传入的是Activity的Context:当这个Context所对应的Activity退出时,由于该Context和Activity的生命周期一样长(Activity间接继承于Context),所以当前Activity退出时它的内存并不会被回收,因为单例对象持有该Activity的引用。

  1. 实例代码:
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;
    }
}
           

4.解决方案

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.5非静态内部类创建静态实例造成的内存泄漏

有的时候我们可能会在启动频繁的Activity中,为了避免重复创建相同的数据资源,可能会出现这种写法:

public class MainActivity extends Activity {
    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

2.6 Handler造成的内存泄漏

这种创建Handler的方式会造成内存泄漏,由于mHandler是Handler的非静态匿名内部类的实例,所以它持有外部类Activity的引用,我们知道消息队列是在一个Looper线程中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏

public class MainActivity extends Activity {

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

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        loadData();
    }

    private void loadData(){
       //...request
       Message message = Message.obtain();
      mHandler.sendMessage(message);
    }
}
           

解决方案:

public class MainActivity extends Activity {
    private MyHandler mHandler = new MyHandler(this);
    private TextView mTextView ;

    private static class MyHandler extends Handler {
        //创建一个静态Handler内部类,然后对Handler持有的对象使用弱引用
        //这样在回收时也可以回收Handler持有的对象,这样虽然避免了Activity泄漏
        private WeakReference<Context> reference;

        public MyHandler(Context context) {
            reference = new WeakReference<>(context);
        }

        @Override
        public void handleMessage(Message msg) {
             MainActivity activity = (MainActivity) reference.get();
             if(activity != null){
                 activity.mTextView.setText("");
             }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mTextView = (TextView)findViewById(R.id.textview);
        loadData();
    }

    private void loadData() {
        //...request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }

    //Looper线程的消息队列中还是可能会有待处理的消息,
    //所以我们在Activity的Destroy时或者Stop时应该移除消息队列中的消息
    @Override
    protected void onDestroy() {
        super.onDestroy();
        //移除消息队列中所有消息和所有的Runnable
        mHandler.removeCallbacksAndMessages(null);
    }
}
           

创建一个静态Handler内部类,然后对Handler持有的对象使用弱引用,这样在回收时也可以回收Handler持有的对象,这样虽然避免了Activity泄漏,不过Looper线程的消息队列中还是可能会有待处理的消息,所以我们在Activity的Destroy时或者Stop时应该移除消息队列中的消息,使用mHandler.removeCallbacksAndMessages(null);是移除消息队列中所有消息和所有的Runnable。当然也可以使用mHandler.removeCallbacks();或mHandler.removeMessages();来移除指定的Runnable和Message。

2.7线程造成的内存泄漏

//——————test1
    new AsyncTask<Void, Void, Void>() {
      @Override
      protected Void doInBackground(Void... params) {
        SystemClock.sleep();
        return null;
      }
    }.execute();
//——————test2
    new Thread(new Runnable() {
      @Override
      public void run() {
        SystemClock.sleep();
      }
    }).start();
           

上面的异步任务和Runnable都是一个匿名内部类,因此它们对当前Activity都有一个

隐式引用。如果Activity在销毁之前,任务还未完成,那么将导致Activity的内存资

源无法回收,造成内存泄漏。

正确的做法还是使用静态内部类的方式,如下:

//test1
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();
        return null;
    }

    @Override
    protected void onPostExecute(Void aVoid) {
        super.onPostExecute(aVoid);
        MainActivity activity = (MainActivity) weakReference.get();
        if (activity != null) {
           //...
        }
    }
}

//test2
static class MyRunnable implements Runnable{
    @Override
    public void run() {
        SystemClock.sleep();
    }
}
//——————
new Thread(new MyRunnable()).start();
new MyAsyncTask(this).execute();
           

这样就避免了Activity的内存资源泄漏,当然在Activity销毁时候也应该取消相应的任

务AsyncTask.cancel(),避免任务在后台执行浪费资源。

2.8集合中对象没清理造成的内存泄漏

Vector v = new Vector();

for (int i = ; i < ; i++) {
    Object o = new Object();
    v.add(o);
    o = null;
}
           

通常把一些对象的引用加入到了集合中,当我们不需要该对象时,并没有把它的引用从

集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更

严重了。

循环申请Object对象,并将所申请的对象放入一个Vector中,如果仅仅释放对象本身,

但因为Vector仍然引用该对象,所以这个对象对GC来说是不可回收的。因此,如果对象

加入到Vector后,还必须从Vector中删除,最简单的方法就是将Vector对象设置为nul