android 内存泄漏分析

什么是内存泄漏

一些对象有着有限的生命周期。当这些对象所要做的事情完成了,我们希望他们会被回收掉。但是如果有一系列对这个对象的引用,那么在这个对象生命周期结束的时候,它是不会被回收的。它还会占用内存,这就造成了内存泄露。持续累加,内存很快被耗尽。

比如,当 Activity.onDestroy 被调用之后,activity 以及它涉及到的 view 和相关的 bitmap 都应该被回收。但是,如果有一个后台线程持有这个 activity 的引用,那么 activity 对应的内存就不能被回收。这最终将会导致内存耗尽,然后因为 OOM 而 crash。

内存泄漏的原因

非静态内部类的静态实例容易造成内存泄漏

public class MainActivityextends Activity
{
         static Demo sInstance = null;
    @Override
    public void onCreate(BundlesavedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (sInstance == null)
        {
           sInstance= new Demo();
        }
    }
    class Demo
    {
    voiddoSomething()
    {
               System.out.print("dosth.");
    }
    }
}

上面的代码中的sInstance实例类型为静态实例,在第一个MainActivity act1实例创建时,sInstance会获得并一直持有act1的引用。 当MainAcitivity销毁后重建,因为sInstance持有act1的引用,所以act1是无法被GC回收的,进程中会存在2个MainActivity实例 (act1和重建后的MainActivity实例),这个act1对象就是一个无用的但一直占用内存的对象,即无法回收的垃圾对象。 所以,对于lauchMode不是singleInstance的Activity, 应该避免在activity里面实例化其非静态内部类的静态实例。

activity使用静态成员,Context泄漏

private static Drawable sBackground;  
@Override  
protected void onCreate(Bundle state) {  
  super.onCreate(state);  
  TextView label = new TextView(this);  
  label.setText("Leaks are bad");  
  if (sBackground == null) {  
    sBackground = getDrawable(R.drawable.large_bitmap);  
  }  
  label.setBackgroundDrawable(sBackground);  
  setContentView(label);  
}  

这段代码效率很快,但同时又是极其错误的;因为在android 2.3系统上,它会导致activity销毁后无法被系统回收。 在第一次屏幕方向切换时它泄露了一开始创建的Activity。当一个Drawable附加到一个 View上时, View会将其作为一个callback设定到Drawable上。上述的代码片段,意味着这个静态的Drawable拥有一个TextView的引用, 而TextView又拥有Activity(Context类型)的引用,换句话说,Drawable拥有了更多的对象引用。即使Activity被 销毁,内存仍然不会被释放。 另外,对Context的引用超过它本身的生命周期,也会导致该Context无法回收,从而导致内存泄漏。所以尽量使用Application这种Context类型。 这种Context拥有和应用程序一样长的生命周期,并且不依赖Activity的生命周期。如果你打算保存一个长时间的对象, 并且其需要一个 Context,记得使用Application对象。你可以通过调用Context.getApplicationContext()或 Activity.getApplication()轻松得到Application对象。

下面看看android4.0为了避免上述问题所做的改进。

先看看android 2.3的Drawable.Java对setCallback的实现:

    public final void setCallback(Callback cb){
        mCallback = cb;
}

再看看android 4.0的Drawable.Java对setCallback的实现:

    public final void setCallback(Callback cb){
        mCallback = newWeakReference<Callback> (cb);
}

在android 2.3中要避免内存泄漏也是可以做到的, 在activity的onDestroy时调用

sBackgroundDrawable.setCallback(null)

以上2个例子的内存泄漏都是因为Activity的引用的生命周期超越了activity对象的生命周期。也就是常说的Context泄漏,因为activity就是context。

总结一下避免Context泄漏应该注意的问题: 1.尽量使用Application这种Context类型。 2.注意对Context的引用不要超过它本身的生命周期。 3.慎重的对Context使用“static”关键字。 4.Context里如果有线程,一定要在onDestroy()里及时停掉。

使用handler时的内存问题

我们知道,Handler通过发送Message与主线程交互,Message发出之后是存储在MessageQueue中的,有些Message也不是马上就被处理的。 在Message中存在一个 target,是Handler的一个引用,如果Message在Queue中存在的时间越长,就会导致Handler无法被回收。 如果Handler是非静态的,则会导致Activity或者Service不会被回收。 所以正确处理Handler等之类的内部类,应该将自己的Handler定义为静态内部类。

public classMainActivity extends Activity
{
    @Override
    public void onCreate(BundlesavedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Thread mThread = newHandlerThread("demo", Process.THREAD_PRIORITY_BACKGROUND);
        mThread.start();
MyHandler mHandler = new MyHandler( mThread.getLooper( ) );
…….
…….
…….
}
    @Override
    public void onDestroy()
    {
    super.onDestroy();
    }
}

这个代码存在泄漏问题,因为HandlerThread的run方法是一个死循环,它不会自己结束,线程的生命周期超过了activity生命周期, 当横竖屏切换,HandlerThread线程的数量会随着activity重建次数的增加而增加。

应该在onDestroy时将线程停止掉:mThread.getLooper().quit();

另外,对于不是HandlerThread的线程,也应该确保activity消耗后,线程已经终止,可以这样做:在onDestroy时调用mThread.join();

先看一组简单的代码

public class SampleActivity extends Activity {
  private final Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
      // ...
    }
  }
}

当我们这样写在一个Activity中时,Android Lint会提示我们这样一个 warning: In Android, Handler classes should be static or leaks might occur.。 意思说:在Android中,Handler 类应该是静态的否则可能发生泄漏。

为什么会是这样呢?

了解一下Handler

1.当Android程序第一次创建的时候,在主线程同时会创建一个Looper对象。Looper实现了一个简单的消息队列,一个接着一个处理Message 对象。程序框架所有主要的事件(例如:屏幕上的点击时间,Activity生命周期的方法等等)都包含在Message对象中, 然后添加到Looper的消息队列中,一个一个处理。主线程的Looper存在整个应用程序的生命周期内。 2.当一个Handler对象在主线程中创建的时候,它会关联到Looper的 message queue 。Message添加到消息队列中的时候Message会持有当前Handler引用,当Looper处理到当前消息的时候,会调用Handler#handleMessage(Message). 3.在java中,no-static的内部类会 隐式的 持有当前类的一个引用。static的类则没有。

在什么地方引起了内存的泄露呢?再看看下面一段代码

public class SampleActivity extends Activity {
  private final Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
      // ...
    }
  }
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // 发送一个10分钟后执行的一个消息
    mHandler.postDelayed(new Runnable() {
      @Override
      public void run() { }
    }, 600000);
    // 结束当前的Activity
    finish();
  }
}

当Activity结束后,在 Message queue 处理这个Message之前,它会持续存活着。这个Message持有Handler的引用, 而Handler有持有Activity(SampleActivity)的引用,这个Activity所有的资源,在这个消息处理之前都不能也不会被回收,所以发生了内存泄露。

解决办法,看下面一段代码

public class SampleActivity extends Activity {
  /** * 使用静态的内部类,不会持有当前对象的引用 */
  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);
  /** * 使用静态的内部类,不会持有当前对象的引用 */
  private static final Runnable sRunnable = new Runnable() {
      @Override
      public void run() { }
  };
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // 发送一个10分钟后执行的一个消息
    mHandler.postDelayed(sRunnable, 600000);
    // 结束
    finish();
  }
}

注册某个对象后未反注册

注册广播接收器、注册观察者等等,比如:

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

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

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

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

我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。

资源对象没关闭造成的内存泄露

资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。 它们的缓冲不仅存在于java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄露。 因为有些资源性对象,比如SQLiteCursor(在析构函数finalize(),如果我们没有关闭它,它自己会调close()关闭),如果我们没有关闭它, 系统在回收它时也会关闭它,但是这样的效率太低了。因此对于资源性对象在不使用的时候,应该调用它的close()函数,将其关闭掉, 然后才置为null.在我们的程序退出时一定要确保我们的资源性对象已经关闭。

程序中经常会进行查询数据库的操作,但是经常会有使用完毕Cursor后没有关闭的情况。如果我们的查询结果集比较小,对内存的消耗不容易被发现, 只有在常时间大量操作的情况下才会复现内存问题,这样就会给以后的测试和问题排查带来困难和风险。

    原文作者:mxn原创
    原文地址: http://souly.cn/%E6%8A%80%E6%9C%AF%E5%8D%9A%E6%96%87/2015/05/13/android-%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E5%88%86%E6%9E%90/
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞