Android技术分享| Android 中部分内存泄漏示例及解决方案

655 阅读4分钟

简单介绍内存泄漏&内存抖动

内存泄漏

Memory leak, 是一种资源泄漏,主因是计算机程序对存储器配置管理失当,失去对一段已分配内存空间的控制,造成程序继续占用已经不再使用的内存空间,或是存储器所存储之对象无法透过执行代码而访问,令内存资源空耗。

简单来说,内存泄漏 是指无法正确回收已经不再使用的内存

举例:

请注意以下的例子是虚构的

在此例中的应用程序是一个简单软件的一小部分,用来控制电梯的运作。
此部分软件当乘客在电梯内按下一楼层的按钮时运行。

当按下按钮时:

要求使用存储器,用作记住目的楼层
把目的楼层的数字储存到存储器中
电梯是否已到达目的楼层?
如是,没有任何事需要做:程序完成
否则:
等待直至电梯停止
到达指定楼层
释放刚才用作记住目的楼层的存储器

此程序有一处会造成存储器泄漏:如果在电梯所在楼层按下该层的按钮(即上述程序的第4步),程序将触发判断条件而结束运行,但存储器仍一直被占用而没有被释放。这种情况发生得越多,泄漏的存储器也越多。

这个小错误不会造成即时影响。因为人不会经常在电梯所在楼层按下同一层的按钮。而且在通常情况下,电梯应有足够的存储器以应付上百次、上千次类似的情况。不过,电梯最后仍有可能消耗完所有存储器。这可能需要数个月或是数年,所以在简单的测试下这个问题不会被发现。

而这个例子导致的后果会是不那么令人愉快。至少,电梯不会再理会前往其他楼层的要求。更严重的是,如果程序需要存储器去开启电梯门,那可能有人被困电梯内,因为电梯没有足够的存储器去开启电梯门。

存储器泄漏只会在程序运行的时间内持续。例如:关闭电梯的电源时,程序终止运行。当电源再度开启,程序会再次运行而存储器会重置,而这种缓慢的泄漏则会从头开始再次发生。

内存抖动

源自Android文档中的Memory churn一词,中文翻译为内存抖动。 指快速频繁的创建对象从而产生的性能问题。

引用Android文档原文:

垃圾回收事件通常不会影响应用的性能。不过,如果在短时间内发生许多垃圾回收事件,就可能会快速耗尽帧时间。系统花在垃圾回收上的时间越多,能够花在呈现或流式传输音频等其他任务上的时间就越少。

通常,“内存抖动”可能会导致出现大量的垃圾回收事件。实际上,内存抖动可以说明在给定时间内出现的已分配临时对象的数量。

例如,您可以在 for 循环中分配多个临时对象。或者,您也可以在视图的 onDraw() 函数中创建新的 PaintBitmap 对象。在这两种情况下,应用都会快速创建大量对象。这些操作可以快速消耗新生代 (young generation) 区域中的所有可用内存,从而迫使垃圾回收事件发生。

内存泄漏(Memory leak)的产生和避免方式

Java内存泄漏的根本原因是长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏。

尽管短生命周期对象已经不再需要,但因为长生命周期依旧持有它的引用,故不能被回收而导致内存泄漏。

几种引起内存泄漏的问题:

静态集合类引起的内存泄漏

HashMapArrayList等集合以静态形式声明时,这些静态对象的生命周期与应用程序一致。他们所引用的对象也无法被释放,因为它们也被集合引用着。

private static HashMap<String, Object> a = new HashMap();
public static void main(String args[]) {
  for (int i = 0; i < 1000; i++) {
    Object tO = new Object();
    a.put("0", tO);
    tO = null;  
  }
}

如果仅仅释放引用本身(tO = null),ArrayList依然在引用该对象,GC无法回收

监听器

在Java应用中,通常会用到很多监听器,一般通过addXXXXListener()实现。但释放对象时通常会忘记删除监听器,从而增加内存泄漏的风险。

各种连接

如数据库连接、网络连接(Socket)和I/O连接。忘记显式调用close()方法引起的内存泄漏

内部类和外部模块的引用

内部类的引用是很容易被遗忘的一种,一旦没有释放可能会导致一系列后续对象无法释放。此外还要小心外部模块不经意的引用,内部类是否提供相应的操作去除外部引用。

单例模式

由于单例的静态特性,使其生命周期与应用的生命周期一样长,一旦使用不恰当极易造成内存泄漏。如果单利持有外部引用,需要注意提供释放方式,否则当外部对象无法被正常回收时,会进而导致内存泄漏。

常见的内存泄漏处理方式:

集合类泄漏

如集合的使用范围超过逻辑代码的范围,需要格外注意删除机制是否完善可靠。比如由静态属性static指向的集合。

单利泄漏

以下为简单逻辑代码,只为举例说明内存泄漏问题,不保证单利模式的可靠性

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;
  }
}

AppManager创建时需要传入一个Context,这个Context的生命周期长短至关重要。

  1. 如果传入的是ApplicationContext,因为Application的生命周期等同于应用的生命周期,所以没有任何问题
  2. 如果传入的是ActivityContext,则需要考虑这个Activity是否在整个生命周期都不会被回收了,如果不是,则会造成内存泄漏

非静态内部类创建静态实例造成的内存泄漏

public class MyActivity extends AppCompatActivity {
  private static MyInnerClass mInnerClass = null;

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

    if (mInnerClass == null) {
      mInnerClass = new MyInnerClass();
    }
  }

  class MyInnerClass {
    ...
  }
}

内部类持有外部类引用,而static声明的对象声明周期通常会比Activity长。即使关闭这个页面,由于mInnerClass为静态的,并且持有MyActivity的引用,导致无法回收此页面从而引起内存泄漏

应该将该内部类单独封装为一个单例来使用。

匿名内部类/异步线程

public class MyActivity extends AppCompatActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ...

    new Thread(new Runnable() {
      @Override
      public void run() {
        ...
      }
    }).start();
  }
}

Runnable都使用了匿名内部类,将持有MyActivity的引用。如果任务在Activity销毁前未完成,将导致Activity的内存无法被回收,从而造成内存泄漏

解决方法:将Runnable独立出来或使用静态内部类,可以避免因持有外部对象导致的内存泄漏

Handler造成的内存泄漏

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

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

    mHandler.postDelayed(new Runnable() {
      @Override
      public void run() {
        ...
      }
    }, 300000);

    finish();
  }
}

Handler属于TLS(Thread Local Storage)变量,生命周期与Activity是不一致的,容易导致持有的对象无法正确被释放

当Android应用程序启动时,该应用程序的主线程会自动创建一个Looper对象和与之关联的MessageQueue。

当主线程中实例化一个Handler对象后,它就会自动与主线程Looper的MessageQueue关联起来。所有发送到MessageQueue的Messag都会持有Handler的引用,所以Looper会据此回调Handle的handleMessage()方法来处理消息。只要MessageQueue中有未处理的Message,Looper就会不断的从中取出并交给Handler处理。

另外,主线程的Looper对象会伴随该应用程序的整个生命周期。

在Java中,非静态内部类和匿名类内部类都会潜在持有它们所属的外部类的引用,但是静态内部类却不会。

当该 Activity 被 finish() 掉时,延迟执行任务的 Message 还会继续存在于主线程中,它持有该 Activity 的 Handler 引用,所以此时 finish() 掉的 Activity 就不会被回收了从而造成内存泄漏(因 Handler 为非静态内部类,它会持有外部类的引用,在这里就是指 SampleActivity)。

解决方法:在 Activity 中避免使用非静态内部类,比如上面我们将 Handler 声明为静态的,则其存活期跟 Activity 的生命周期就无关了。同时通过弱引用的方式引入 Activity,避免直接将 Activity 作为 context 传进去,见如下代码:

public class SampleActivity extends AppCompatActivity {

  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 mRunnable = new Runnable() {
    @Override
    public void run() { ... }
  }

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

    mHandler.postDelayed(mRunnable, 300000);
    finish();
  }
}
避免不必要的静态成员变量

对于BroadcastReceiver、ContentObserver、File、Cursor、Stream、Bitmap等资源的使用,应在Activity销毁前及时关闭或注销

不使用WebView对象时,应调用destroy()方法销毁

在这里插入图片描述